William Ghelfi
Apr 18 · 5 min read

So far you learned how to pass data into components via @Inputs.
Let’s see how to react to user generated events with @Outputs.

Photo by Kelly Sikkema on Unsplash

Empty state

We are going to refactor the application to make it start with an initial empty state, then load the greeting cards on demand after the user clicks on a button.

Update src/app/app.component.ts:

import { Component } from '@angular/core';// rxjs
import { Observable } from 'rxjs';
// App Models
import { Person } from './persons/person.model';
// App Services
import { PersonsService } from './persons/persons.service';
@Component({
selector: 'ng4d-root',
template: `
<ng-container *ngIf="(persons$ | async) as persons; else emptyStateTemplate">
<ng4d-greeting-card *ngFor="let person of persons" [person]="person"></ng4d-greeting-card>
</ng-container>
<ng-template #emptyStateTemplate>
<p>There nothing to see here :-(</p>
<p>
<button type="button">Load greeting cards</button>
</p>
</ng-template>
`,
})
export class AppComponent {
// Always append a `$` - for stream - to an Observable property, for clarity
persons$: Observable<Person[]>;
constructor(private personsService: PersonsService) {
// this.persons$ = this.personsService.getPersons();
}
}

What we’ve done:

  • Used ngIf to conditionally display portions of a template, with thengIf as syntax to make it simpler to work with observable data via the async pipe
  • Applied the ngIf to ng-container, a utility component, just because you can’t apply more than one structural directive (ngIf and ngFor) to the same component
  • Used the ngIf; else syntax to have a “cards loaded” state and a “no cards loaded” state (or “empty state”)
  • Wrapped our empty state portion of template into another utility component: ng-template
  • Temporarily commented out the use of PersonsService to load the data to feed into the GreetingCardComponents

The extra mile for good measure

Before going on and make that new button actually load the data we need, let’s follow the Angular Way here and let’s extract the empty state into its own component.

And let’s also prepare a new “Greeting Cards” feature while we are at it.

  • Rename src/greeting-card into src/greeting-cards, and update all the relevant imports throughout the application
  • Rename src/greeting-cards/greeting-card.component into src/greeting-cards/greeting-cards-item.component.ts; update its selector to ng4d-greeting-cards-item, and its class name to GreetingCardsItemComponent.
    Also update all the relevant imports.
  • Create a new component: src/greeting-cards/greeting-cards-list.component.ts, refactoring the template of AppComponent into the template of this new component:
import { Component, Input } from '@angular/core';// App Models
import { Person } from '../persons/person.model';
@Component({
selector: 'ng4d-greeting-cards-list',
template: `
<section *ngIf="persons.length > 0; else emptyStateTemplate"
<ng4d-greeting-cards-item
*ngFor="let person of persons"
[person]="person"
></ng4d-greeting-cards-item>
</section>
<ng-template #emptyStateTemplate>
<ng4d-greeting-cards-empty-state></ng4d-greeting-cards-empty-state>
</ng-template>
`,
})
export class GreetingCardsListComponent {
@Input() persons: Person[];
}
  • Create a new component: src/greeting-cards/greeting-cards-empty-state.component.ts:
import { Component } from '@angular/core';@Component({
selector: 'ng4d-greeting-cards-empty-state',
template: `
<p>There nothing to see here :-(</p>
<p>
<button type="button">Load greeting cards</button>
</p>
`,
})
export class GreetingCardsEmptyStateComponent {}
  • Create a module for the new feature, src/greeting-cards/greeting-cards.module.ts:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
// App Components
import { GreetingCardsListComponent } from './greeting-cards-list.component';
import { GreetingCardsItemComponent } from './greeting-cards-item.component';
import { GreetingCardsEmptyStateComponent } from './greeting-cards-empty-state.component';
@NgModule({
exports: [GreetingCardsListComponent],
declarations: [
GreetingCardsListComponent,
GreetingCardsItemComponent,
GreetingCardsEmptyStateComponent,
],
imports: [CommonModule],
})
export class GreetingCardsListModule {}
  • Import the new module into AppModule so that we can use its exported component inside the template of AppComponent:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
// App Components
import { AppComponent } from './app.component';
import { GreetingCardsItemComponent } from './greeting-cards/greeting-cards-item.component';
// App Modules
import { PersonsModule } from './persons/persons.module';
import { GreetingCardsModule } from './greeting-cards/greeting-cards.module';
@NgModule({
declarations: [AppComponent, GreetingCardsItemComponent],
imports: [BrowserModule, PersonsModule, HttpClientModule, GreetingCardsModule],
bootstrap: [AppComponent],
})
export class AppModule {}
  • Update AppComponent:
import { Component } from '@angular/core';// rxjs
import { Observable } from 'rxjs';
// App Models
import { Person } from './persons/person.model';
// App Services
import { PersonsService } from './persons/persons.service';
@Component({
selector: 'ng4d-root',
template: `
<ng4d-greeting-cards-list
[persons]="persons$ | async"
></ng4d-greeting-cards-list>
`,
})
export class AppComponent {
// Always append a `$` - for stream - to an Observable property, for clarity
persons$: Observable<Person[]>;
constructor(private personsService: PersonsService) {
// this.persons$ = this.personsService.getPersons();
}
}

So far so good, now for the fun part...

Loading data on demand

  • Update GreetingCardsEmptyStateComponent to make it react to the click of that button and emit an event to the outside world:
import { Component, Output, EventEmitter } from '@angular/core';@Component({
selector: 'ng4d-greeting-cards-empty-state',
template: `
<p>There nothing to see here :-(</p>
<p>
<button
type="button"
(click)="emitDataRequested($event)"
>Load greeting cards</button>
</p>
`,
})
export class GreetingCardsEmptyStateComponent {
@Output() dataRequested = new EventEmitter<void>();
emitDataRequested(event: Event) {
event.preventDefault();
this.dataRequested.emit();
}
}

Bubble that output / event up to AppComponent

AppComponent here is the smart component, whereas the other components are all dumb components.
Smart components direct, organize, triage, the data stream and perform most of the business logic (but this last role will experience a change of ownership once we get to talk about NGRX in the coming stories).
Dumb components get data, display data, and emit outputs.
Depending on the size of your application, you may want one smart component per page, and/or one per feature.
This application is still simple enough to leave it all to AppComponent.

  • Update GreetingCardsListComponent:
import { Component, Input, Output, EventEmitter } from '@angular/core';// App Models
import { Person } from '../persons/person.model';
@Component({
selector: 'ng4d-greeting-cards-list',
template: `
<section *ngIf="persons && persons.length > 0; else emptyStateTemplate">
<ng4d-greeting-cards-item
*ngFor="let person of persons"
[person]="person"
></ng4d-greeting-cards-item>
</section>
<ng-template #emptyStateTemplate>
<ng4d-greeting-cards-empty-state
(dataRequested)="emitDataRequested()"
></ng4d-greeting-cards-empty-state>
</ng-template>
`,
})
export class GreetingCardsListComponent {
@Input() persons: Person[];
@Output() dataRequested = new EventEmitter<void>(); emitDataRequested() {
this.dataRequested.emit();
}
}
  • Update AppComponent to receive the output and react by loading the data:
import { Component } from '@angular/core';// rxjs
import { Observable } from 'rxjs';
// App Models
import { Person } from './persons/person.model';
// App Services
import { PersonsService } from './persons/persons.service';
@Component({
selector: 'ng4d-root',
template: `
<ng4d-greeting-cards-list
[persons]="persons$ | async"
(dataRequested)="requestData()"
></ng4d-greeting-cards-list>
`,
})
export class AppComponent {
// Always append a `$` - for stream - to an Observable property, for clarity
persons$: Observable<Person[]>;
constructor(private personsService: PersonsService) {} requestData() {
this.persons$ = this.personsService.getPersons();
}
}

And it’s working!

Loading all the things

Wrapping up

This time you learned how to:

  • Emit user generated events like the click of a button
  • Organize you application in features, and your features in dumb components and smart components
  • Bubble user generated events up to smart components and act on them

Next time you’ll learn how to add some styling with Bootstrap, and then we will talk about adding new persons with Angular’s powerful FormBuilder.


Angular for dads

You just don’t have time, I’ve been there. But you can still learn Angular.ˇ

William Ghelfi

Written by

http://octohire.me / Born, Growing Up / Author of Bootstrap In Practice: http://www.williamghelfi.com/bootstrap-in-practice/

Angular for dads

You just don’t have time, I’ve been there. But you can still learn Angular.ˇ

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade