Angular Component Communication

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:
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

  • Binding
  • Structural Directives
  • ViewChild/ViewChildren with template reference variable and nativeElement
  • ViewChild/ViewChildren with NgForm or NgModel

Change Notification

  • Two-way binding
  • Getters / setters
  • ViewChild and valueChanges observable

 

Parent to Child

  • Input properties (@Input)
  • Template reference variables
  • ViewChild decorators

Change Notification

  • Getters / setters
  • onChanges lifecycle hook

Child to Parent

  • Output properties (@Output) and using emitters
Service

  • Simple Properties
  • Getters / setters
  • State Management
  • Subject
  • BehaviorSubject
  • ngrx/Redux

Router

  • Required parameter
  • Optional parameter
  • Query parameter

 

 

References

Angular Component Communication by Deborah Kurata