Working with user input in Angular. Part 1: outputs

Working with user input in Angular.

Part 1: outputsWilliam GhelfiBlockedUnblockFollowFollowingApr 18So 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 UnsplashEmpty stateWe 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';// rxjsimport { Observable } from 'rxjs';// App Modelsimport { Person } from '.

/persons/person.

model';// App Servicesimport { 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 pipeApplied 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 componentUsed 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-templateTemporarily commented out the use of PersonsService to load the data to feed into the GreetingCardComponentsThe extra mile for good measureBefore 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 applicationRename 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 Modelsimport { 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 Componentsimport { 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 Componentsimport { AppComponent } from '.

/app.

component';import { GreetingCardsItemComponent } from '.

/greeting-cards/greeting-cards-item.

component';// App Modulesimport { 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';// rxjsimport { Observable } from 'rxjs';// App Modelsimport { Person } from '.

/persons/person.

model';// App Servicesimport { 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 demandUpdate 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 AppComponentAppComponent 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 Modelsimport { 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';// rxjsimport { Observable } from 'rxjs';// App Modelsimport { Person } from '.

/persons/person.

model';// App Servicesimport { 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 thingsWrapping upThis time you learned how to:Emit user generated events like the click of a buttonOrganize you application in features, and your features in dumb components and smart componentsBubble user generated events up to smart components and act on themNext 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.

Previous: Feeding data from a REST endpoint to Angular components.. More details

Leave a Reply