Note: material below were from Angular course by Deborah Kurata (see link below). Below are my notes and her charts taken from that course.
Some situations where commuication takes place in an Angular application:
- Component to Template (Child Components)
- View updates
- React to user event
- Perform a task
- Check form or control state
- Component to Component (Service)
- State
- Sharing data
- Notifications (event)
- Component to Router
- Pass data through router
Communicating with Template
The following ways are available for component-to-template communication:
- Interpolation
- Using blocks {{ }}
- Property Binding
- Using bracket []
- Event Binding
- Using parenthesis ()
- Two way Binding
- Using bracket parenthesis / football [()]
Structural Directives
Some structural directives are used to control elements in the view. For example:
*ngIf <img *ngIf='showImage' ...> *ngFor
<table *ngIf='products?.length > 0'> <tr *ngFor='let product of products'>
Notifying Component of User Event
There are a few ways in notifying a component of a user change:
- Two-way binding
- Is a combination of property binding and event binding using [()]
- This can be split up as shown below. The following 3 lines could perform the identical functions.
<input type='text' [(ngModel)]='listfilter' /> <input type='text' [ngModel]='listfilter' (ngModelChange)='listfilter = $event'> <input type='text' [ngModel]='listfilter' (ngModelChange)='onFilterChange($event)'>
Note that in the last example above, we are no longer doing two-way binding. We are breaking that down to use the ‘ngModelChange’ function.
- Getter and setter
- Typescript allows two different ways to define variables
- Property Declaration
-
listFilter: string
-
- Getter / Setter – everything user changes on the template it calls the setter method in the component class:
- Property Declaration
- Typescript allows two different ways to define variables
private _listfilter: strin getlistFilter(): string { setlistFilter(vulae: string){this._listfilter=value;}
- valueChanges observerable
- This uses ViewChild and ViewChildren
- Examined further in the next section
ViewChild and ViewChildren
ViewChild is used for component to template communication. It is used to reference a directive or element. For example, below shows how it was done in traditional JS using the DOM and how Angular does it using the ViewChild decorator.
let element = document.getElementById('elementId'); @ViewChild('elementVariable') elementRef; // template reference variable @ViewChild(NgModel) filterInput: NgModel; // angular directive - on template would look like: <input [(ngModel)]='listfilter'>... @ViewChild(StarComponent) star: StarComponent // child component - on template would look like: <custom-star [input1]='some.input'>...
The @ViewChild decorator takes a selector – which is the name of the template reference variable (‘elementVariable’ as shown above), or an angular control or directive on the template. It also be a child component, as shown below.
Note that when using a template reference variable it will always be initialized to null. The reason for this is that the component is always constructed and intialized first, then the view template is initialized and rendered. Therefore in the component’s constructor or ngOnInit the template reference will always be null. If we need to reference variables after the template is rendered, we can use the Angular life-cycle hook called AfterViewInit.
The note above can be useful when needing to set default behaviors on page load. For example, if we need the cursor to be focused on the ‘elementVariable’ element, we could do that by having the following template and component definitions.
Template: <inputtype='text' #filterElement [(ngModel)]='listFilter' /> Component: ngAfterViewInit():void { this.filterElementRef.nativeElement.focus(); }
Like ViewChild, we can also use ViewChildren which returns a QueryList of elements or directive references. It tracks changes done on that DOM element if used against an element. But ViewChildren can also be used for angular directives (like NgModel) as well as custom directives or child components. In these cases the ViewChildren tracks all references to those child components.
@ViewChildren('elementVariable') elementRefs: QueryList<ElementRef>; // template reference variable @ViewChildren('elementVariable1, elementVariable2') elementRefs: QueryList<ElementRef>; // template reference to multiple variables @ViewChild(NgModel) inputs: QueryList<NgModel>; // references all instances of NgModel directive @ViewChild(StarComponent) stars: QueryList<StarComponent> // child component - on template would look like: <custom-star [input1]='some.input'>...
It should be noted that the example above is directly accessing the DOM and therefore creating a tightly coupled reference. This can be disadvantageous for several reasons. Below are some pros and cons for using each of the different types of selectors.
- ViewChild with HTML Element – @ViewChild(‘elementId’)
- Provides native DOM element property
- Able to access any HTML element
- Can call HTML element methods
- The ViewChild reference is initially null (until template is rendered)
- ViewChild is null if the element is not in the DOM
- Does not work with server-side rendering or web workers
- ViewChild with Angular Directive – @ViewChild(NgModel)
- Provides reference to all of that directive’s data structures and properties
- Like the HTML element, this is only available after template rendered and initially null
- Like the HTML element, it is not available if not in DOM
- Supports NgForm and NgModel, but are read only
- ViewChild with valueChanges Observerable – this.viewchildvar.valueChanges.subscribe(…)
- This can be problematic when using with ngIf
- Reference is available after template rendered, initially null
Using ViewChild in Angular Forms
There are two types of Angular Forms
- Template Driven
- Angular creates form data structures
- Based on info in template
- Access reference with Viewchild
- Reactive
- We create form data structures
- Defined in component class
- No need for viewchild
Communicating with Child Component
In order for parent component to be able to communicate with a child component, the child component must be contained in the parent component’s template. Therefore when using routing communication the parent component would not be able to communicate with the child (would have to use routing communication techniques). For example, if custom-row is a child component, the parent component must have it defined in it’s template like so:
<pm-criteria #filterCriteria class='col-md-10' [displayDetail]='includeDetail' [hitCount]='filteredProducts?.length'> </pm-criteria>
In the child component it must have the selector defined in it’s decorator. For example:
@Component({ selector: 'pm-criteria', templateUrl: './criteria.component.html', styleUrls: ['./criteria.component.css'] }) export class CriteriaComponent implements OnInit, OnChanges, AfterViewInit { listFilter: string; @Input() displayDetail: boolean; @Input() hitCount: number; hitMessage: string;
The parent can communicate with the child using an @Input decorator on the child. If the parent wants to pull information from the child, it needs to reference that child component using ViewChild decorator. In the example above we can see this with the ‘displayDetail’ and ‘hitCount’ fields.
Also note that a service could be used for both child / parent communication.
Watching for changes on an Input Property in the Child Component
This can be done by using a Getter/Setter on the input variable. Or we can setup the OnChanges life-cycle hook so that it detect the change. For example, if in the sample code above if we want to watch the ‘hitcounter’ variable we would setup something like so:
ngOnChanges(changes:SimpleChanges):void { if (changes['hitCount'] &&!changes['hitCount'].currentValue) { this.hitMessage='No matches found'; } else { this.hitMessage='Hits:'+this.hitCount; } }
The OnChanges hook is called with a SimpleChanges parameter. This parameter contains the objects that are in change.
Calling methods or properties in a child component from parent
In the parent template we can add an template reference variable in the child component element to call methods and properties in that child. This can be seen in the example above by the ‘#filterCriteria’ template reference variable. We can also use the component name as selector. Both examples are shown below in the parent component:
@ViewChild('#filterCriteria') filterComponent: CriteriaComponent; or @ViewChild(CriteriaComponent) filterComponent: CriteriaComponent;
Communicating with Parent Component
Some examples of when a child component would communicate with the parent:
- Event notification
- Use an @Output decorator
- Provide update or information
- Parent use a template reference variable to access the child’s properties
- Use a service
When using Event Notifications the child component needs to define an @Output decorator and “emit” the event. On the parent template we need to capture that event through an event binding. For example, in the child component we have the following decorator and getter/setter on a variable that would trigger/emit the event.
@Output() valueChange: EventEmitter<string> = newEventEmitter<string>(); ... private_listFilter:string; getlistFilter():string { returnthis._listFilter; } setlistFilter(value:string) { this._listFilter=value; this.valueChange.emit(value); }
On the parent component we would capture this event by binding it to a handler method on the template. In the component class we have the code for that method.
Template: <pm-criteria #filterCriteriaclass='col-md-10' [displayDetail]='includeDetail' [hitCount]='filteredProducts?.length' (valueChange)='onValueChange($event)'> </pm-criteria> Class: onValueChange(value:string):void { this.performFilter(value); }
Communicating with Service
Services can be used as an intermediary for component communication. Services are used as a medium for storage. This storage could be accessed by multiple components. This is useful for storing state-based data, such as a property bag.
Types of state data that can be stored:
- View State
- User Information
- Entity Data
- User Selection and Input
These types of state can be stored using
- Property Bag
- Basic object for storing properties
- Basic State Management
- Can use change detection
- State Management with Notifications
- ngrx/Redux
- State is immutable
- actions
- reducers
- store
Storing Filter Criterias us a Property Bag
Sometimes during navigation we want to store the state, such as user filter selection criteria. Services provide functionality across all components that inject it. The service classes are essentially singletons that are instantiated only once throughout the application. Services are also useful for logging, doing domain calculations, data access and data sharing.
The example below shows as service used as a Property Bag – to keep the state of the search filter criteria field.
@Injectable() export class ProductParameterService { showImage:boolean; filterBy:string; constructor() { } }
The parent component would use this service by injecting the service in its constructor, as follows
constructor(private productService:ProductService, private productParameterService:ProductParameterService) { }
The service ca be called from either parent or child components.
Service Scope
When a parent component registers a service, it is available to all child components. The scope rules are same as any other type of service. It is only instantiated and visible per each node and children beneath it in the Angular app hierarchy.
Communicating through State Management Service
A state management service can be used to store certain data from the backend server so that we dont have to make repeated requests. The example below shows how the products data is only retrieved if the array is not set. Note that since the getProducts() method returns an Observable, we do a “return of” allowing the caller to get the Observerable. Also note that the below uses the latest ReactiveJS version 5+ which allows the use of “tap” operators.
@Injectable() export class ProductService { private productsUrl = 'api/products'; private products: IProduct[]; currentProduct: IProduct | null; constructor(private http: HttpClient) { } getProducts(): Observable<IProduct[]> { if (this.products) { return of(this.products); } return this.http.get<IProduct[]>(this.productsUrl) .pipe( tap(data => console.log(JSON.stringify(data))), tap(data => this.products = data), catchError(this.handleError) ); }
Note that we could also put in a timer to simulate a caching mechanism such that the products list is updated when the timer is up. Other strategies are to get the products list on change actions only, such as when deleting, updating or creating products. We could also do this in memory on the front end and still eliminate the backend callback if we would like as well.
Communicating through Service Notification
Services can broadcast notifications to any component that is listening to that service. This could be done using an Event Emitter and using an Output decorator on the service. But this is done usually for child components and not recommended for services. The recommended strategy is to use a subject.
Subject
Subject is a special type of Observerable. It follows the pub-sub model where any subscribed Component would get the broadcasted message. The broadcast is done using the “next” method. The components wanting to subscribe would need to setup a variable pointing to this Observerable property. Example below.
@Injectable() export class ProductService { private productsUrl = 'api/products'; private products: IProduct[]; private selectedProductSource = new BehaviorSubject(null); selectedProductChanges$ = this.selectedProductSource.asObservable(); // $ indicates this is an observable variable constructor(private http: HttpClient) { } changeSelectedProduct(selectedProduct: IProduct | null): void { this.selectedProductSource.next(selectedProduct); // broadcasts the notification } ... private createProduct(product: IProduct, headers: HttpHeaders): Observable { product.id = null; return this.http.post(this.productsUrl, product, { headers: headers} ) .pipe( tap(data => console.log('createProduct: ' + JSON.stringify(data))), tap(data => { this.products.push(data); this.changeSelectedProduct(data); // item was created so broadcast notification of newly selectedProduct }), catchError(this.handleError) ); }
@Component({ selector: 'pm-product-shell-list', templateUrl: './product-shell-list.component.html' }) export class ProductShellListComponent implements OnInit, OnDestroy { pageTitle: string = 'Products'; errorMessage: string; products: IProduct[]; selectedProduct: IProduct | null; sub: Subscription; constructor(private productService: ProductService) { } ngOnInit(): void { this.sub = this.productService.selectedProductChanges$.subscribe( selectedProduct => this.selectedProduct = selectedProduct ); this.productService.getProducts().subscribe( (products: IProduct[]) => { this.products = products; }, (error: any) => this.errorMessage = error ); }
Communicating using Router
RouterLink directive
<a [routerLink]=...>Link</a>
Looks up the router path in the route configuration. If found it will load the specified component and display it into the <router-outlet> directive.
Router based communication can be done using route parameters. The types of parameters are:
- Required
- Optional
- Query
The required parameters are those that are defined in the route configuration. For example in below it would be the Id parameters which are required.
RouterModule.forChild([ { path: '', component: ProductShellComponent }, { path: ':id', component: ProductDetailComponent },
An optional parameter are query parameters that are not required. It is not in the route configuration. Instead it is only found when calling the route, for example:
<a [routerLink] = "['/products', {name: nameVar, code: codeVar}]">Link</a> // from template this.router.navigate(['/products', {name: nameVar, code: codeVar}]); // from component class
The above routes could result in a url like this:
http://localhost/products;name=namvar;code=codevar
On the component executed at this route, it can read the route parameters like this:
this.route.snapshot.paramMap.get('name');
The query parameter is very similar to the optional parameter above except that it has an additional directive. It may look like:
<a [routerLink] = "['/products']" [queryParams]="{name: nameVar, code: codeVar}">Link</a> // from template this.router.navigate(['/products'], {queryParams: {name: nameVar, code: codeVar}}); // from component class
However when reading a query parameter it is slightly different syntax because the generated Url is slightly different.
http://localhost/products?name=namvar&code=codevar this.route.snapshot.queryParamMap.get('name');
Summary
Component to Component | Component to Child Component | Component to Component |
Communication
Change Notification
|
Parent to Child
Change Notification
Child to Parent
|
Service
Router
|
References
Angular Component Communication by Deborah Kurata