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
- Use a child component when:
- for a specific task
- complex
- reusable
@Input
, getter/setter, andOnChanges
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.
- The key here is that it's
- Template reference variable if you want to use it in the parent's template
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:
- Property bag
- Basic state management
- State management with notifications
- 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.
- Provide state values
- Maintain and update state
- 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