Angular Component Communication

My notes from Deborah Kurata's excellent Angular Component Communication.

Module 3

Binding and structural directives

Interpolation

// Component
pageTitle: string = 'Product List';
<!-- Template -->
<div>{{pageTitle}}</div>

Can also use functions ({{ getPageTitle() }}) but may run into performance issues due to how Angular checks for changes.

Property binding

// Component
imageWidth: number = 50;
<!-- Template -->
<img [style.width.px]="imageWidth" />

Event binding

// Component
toggleImage(): void {
	this.showImage = !this.showImage;
}
<!-- Template -->
<button (click)="toggleImage()">Toggle Image</button>

Two-way binding

// Component
listFilter: string;
<!-- Template -->
<input type="text" [(ngModel)]="listFilter" />

*ngIf

// Component
showImage: boolean = false;
<!-- Template -->
<img *ngIf="showImage" [src]="product.imageUrl" />

*ngFor

// Component
products: IProduct[];
<!-- Template -->
<tr *ngFor="let product of products">

Two-way binding, long way

[(ngModel)]="listFilter" is shorthand for [ngModel]="listFilter" (ngModelChange)="listFilter=$event".

So you could do [ngModel]="listFilter" (ngModelChange)="onFilterChange($event)", where onFilterChange updates listFilter.

Getters and setters

// Component
listFilter: string;
// Component
private _listFilter: string;
get listFilter(): string {
	return this._listFilter;
}
set listFilter(value: string) {
	this._listFilter = value;
}

Module 4

In my opinion, using these has more downsides than benefits.

ViewChild

// Angular Directive
@ViewChild(NgModel) filterInput: NgModel; // <input type="text" [(ngModel)]="listFilter" />
// Custom Directive or Child Component
@ViewChild(StarComponent) star: StarComponent;
// Template Reference Variable
@ViewChild('divElementVar') divElementRef: ElementRef; // <div #divElementVar>{{pageTitle}}</div>
// Above is available during/after ngAfterViewInit(), which is after constructor() and ngOnInit().
// However, if within a *ngIf you may run into an issue.
// ElementRef has a nativeElement which allows for access to any HTML element properties or methods.
@ViewChild(NgForm) editForm: NgForm; // if using template-driven forms.

With NgModel we can for example:

@ViewChild(NgModel) filterInput: NgModel;
this.filterInput.valueChanges.subscribe(() => this.performFilter(this.listFilter));

Otherwise, NgModel and NgForm are read-only.

ViewChildren

@ViewChildren(NgModel) inputs: QueryList<NgModel>;
// Above would support checking for status.
@ViewChildren(StarComponent) stars: QueryList<StarComponent>;
@ViewChildren('divElementVar' divElementRefs: QueryList<ElementRef>;
@ViewChildren('filterElement, nameElement' divElementRefs: QueryList<ElementRef>;
// Tracks changes in the DOM.
this.divElementRefs.changes.subscribe(() => { /* act */ });

Module 5

Parent to child component communication

  • Child: @Input, getters/setters, OnChanges
  • Parent: Template reference variable, @ViewChild
  • Use a service

@Input()

Child:

@Input() propertyName: string;

Parent:

<app-child propertyName="binding source"></app-child>

Or:

parentProperty: string;
<app-child [propertyName]="parentProperty"></app-child>

Getter and setter

Child component:

private _propertyName: string;
get propertyName(): string {
	rerturn this._propertyName;
}
@Input() set propertyName(value: string) {
	this._propertyName = value;
}

OnChanges

Child component:

@Input() propertyName: string;

ngOnChanges(changes: SimpleChanges): void {
	// Note: values start at undefined.
	if (changes['propertyName']) {
		changes['propertyName'].currentValue
	}
}

Template reference value

<app-child #childReference [propertyName]="parentProperty"></app-child>

{{ childReference.propertyName }}
{{ childReference.methodName() }}

@ViewChild

<app-child #childReference [propertyName]="parentProperty"></app-child>
@ViewChild('childReference') childComponent: ChildComponent;

or

<app-child [propertyName]="parentProperty"></app-child>
@ViewChild(ChildComponent) childComponent: ChildComponent;
parentPropertyName: string;

ngAfterViewInit(): void {
	this.parentVariable = this.childComponent.propertyName;
}

Summary

  1. Use a child component when:
    • for a specific task
    • complex
    • reusable
  2. @Input, getter/setter, and OnChanges are easier for parent to child
    • Favor getter/setter if you only need to react to changes to specific properties
    • Favor OnChanges if you want to react to any input property changes, or if you need current/previous values
      • The key here is that it's @Input() property changes.
  3. Template reference variable if you want to use it in the parent's template
  4. ViewChild if you want to use it in the class
    • but it won't receive notification of changes

Module 6

Child to parent component communication

  • Event notification: @Output
  • Provide information: Template reference variable, @ViewChild
  • Service

@Output

Child:

@Output() valueChange = new EventEmitter<string>(); // in @angular/core

this.valueChange.emit(value);

Parent:

<app-child (valueChange)="onValueChange($event)"></app-child>
onValueChange(value: string): void {
	// ...
}

Module 7 - Services

Managing state options

From simple to complex:

  1. Property bag
  2. Basic state management
  3. State management with notifications
  4. ngrx (inspired by Redux)

Property bag

Service that just contains properties.

Service:

@Injectable()
export class ThingService {
	propertyName1: string;
	propertyName2: boolean;
}

Component:

get propertyName1(): string {
	return this.thingService.propertyName1;
}
set propertyName1(value: string) {
	this.thingService.propertyName1 = value;
}

constructor(private thingService: ThingService) {
}

Great for 'stashing away properties for itself or other components.'

Service scope

Register the service based upon what you want to be able to use it (scope), and how long it is retained for (lifetime).

  • Register in the component - @Component({ providers: [ ThingService ]}) - for that component and children (template or via router).
    • Good if you need multiple instances of the service for different component instances.
  • Register in a module - @NgModule({ providers: [ ThingService ] }) - no matter which module it's registered in (unless lazy-loaded) it will be available to all components.
    • Lazy-loaded module services are only available to components declared in that module, but is then available for the entire application lifetime.

ngOnDestroy(): void { } if you want to see the lifetime of a service.

Guidelines

Property bag for:

  • Retaining view state
  • Retaining user selections
  • Sharing data or other state
  • Communicating state changes
  • Okay if any component can read or change the values
  • Components are only notified of state changes if they use template binding

Module 8

Basic state management

Essentially store data on the service in a private property, and populate it from the server only when needed.

  1. Provide state values
  2. Maintain and update state
  3. Observe state changes
@Injectable()
export class ThingService {
	private things: IThing[];
	
	getThings(): Observable<IThing[]> {
		if (this.things) {
			return of(this.things);
		}
		// get things from the data store and save to this.things
	}

	getThing(id: number): Observable<IThing> {
		if (this.things) {
			const foundThing = this.things.find(item => item.id === id);
			if (foundThing) {
				return of(foundThing);
			}
		}
		// get thing from the server and return it
	}
}

For create/update/delete either post to the server and update the item in the property list, or pull fresh content from the server, depending upon need.

May want to always pull fresh data when doing an edit.

You may also want to store a pull or expiration date so that you don't have stale data.

Concurrent components

Could put a public property in a property bag or state management service that could be read/updated.

In the component that reads the property, use a getter to return this.thingService.currentThing; if you want it to update every time currentThing is changed.

  • Define a property in the service
  • Bind that property in a template
  • Use a getter in the component class

Note that you'll either need to use binding in the template to have Angular pick up changes or have a timer (import { timer } from 'rxjs/observable/timer'; has one) in ngOnInit that polls for changes. Timers are not ideal.

ngOnInit() {
	timer(0, 1000).subscribe(t => console.log(this.thing));
	// unsubscribe on destroy
}

Module 9

Service notifications

Can add notifications to any service, not just a state management service.

EventEmitter only works child to parent, and you don't want to force it for service notifications.

Use Subject or a variant like BehaviorSubject instead. Subject is a type of Observable, and an Observer. Don't necessarily need, if you can use binding.

Service:

@Injectable()
export class ThingService {
	private selectedThingSource = new Subject<IThing | null>();
	// source = source of knowledge about selected thing
	selectedThingChanges$ = this.selectedThingSource.asObservable();
	// $ convention = observable (that can be subscribed to)
	// could make the source public but then anyone could push to it
	// instead the asObservable makes it read-only externally
	
	changeSelectedThing(selectedThing: IThing | null): void {
		this.selectedThingSource.next(selectedThing);
	}
}

Component, updating:

// ...
this.thingService.changeSelectedThing(thing);
// ...

Component, subscribing:

export class ThingDetailComponent implements OnInit, OnDestroy {
	thing: IThing | null;
	thingSub: Subscription;

	constructor(private thingService: ThingService) { }
	
	ngOnInit(): void {
		this.thingSub = this.thingService.selectedThingChanges$.subscribe(
			selectedThing => this.thing = selectedThing
		);
	}
	
	ngOnDestroy(): void {
		this.thingSub.unsubscribe();
	}
}

BehaviorSubject

Many variants of subject, but this one:

  • requires an initial value
  • provides the current value on a new subscription

Service:

//private selectedThingSource = new Subject<IThing | null>();
private selectedThingSource = new BehaviorSubject<IThing | null>(null);

Works even when components are destroyed. However, will want to make sure the component that updates also subscribes, if it needs to.

Summary

Don't need to use Subject if notifications are not required, or the only notifications are for changes to bound properties.

Subjects can also be used to sync multiple observables (advanced, not covered by this course).

There is a Subject variant that can provide all previous messages.

Module 10

Route parameters

  • Required
  • Optional
  • Query

Required parameters

Define:

{ path: 'products/:id', component: ProductDetailComponent }

Activate:

<a [routerLink]="['/products', product.id]">...</a>
this.router.navigate(['/products', this.product.id]);

Read:

this.route.snapshot.paramMap.get('id');

Optional parameters

In the URL, uses ; as the delimiter. Can be lost during navigation (versus standard query parameters).

Define:

{ path: 'products', component: ProductListComponent }

Activate:

<a [routerLink]="['/products', { name: cart, code: g }]">...</a>
this.router.navigate(['/products', { name: 'cart', code: 'g' }]);

Read:

this.route.snapshot.paramMap.get('name');

Query parameters

In the URL, uses standard ? and &. Can be retained across routes.

Define:

{ path: 'products', component: ProductListComponent }

Activate:

<a [routerLink]="['/products']" [queryParams]="{ name: cart, code: g }">...</a>
this.router.navigate(['/products'], { queryParams: { name: 'cart', code: 'g' }});

Read:

this.route.snapshot.queryParamMap.get('name');

Summary

  • Simple
  • Bookmarkable and sharable
  • Good for small amounts of data