0

Components Interaction in Angular (Part 1)

Since Angular 2.x+ was first introduced, we have become familiar with the architectural principle, in which an application should always be composed of well-encapsulated and loosely-coupled components. In Angular, "everything is components" and the acts of comparing as well as contrasting the way how components communicate with each others in AngularJS (1.x) and the newer version is one of the most magnetic matters to dwell upon.

In the newer versions of Angular, a certain number of communication mediums have been carried over, in which some are repackaged with the same underlying concepts (Input, Output), while some bring out brand new methods used for communication (ViewChild, ContentChild).

Of course, you can look up for all of the overview needed in the Angular Cookbook, still, I would like to describe each mechanism in a more detailed way along with sample code. Let's take a look at these unique forms of communication, the way the work as well as how/when should we use them.

1. Input Bindings

1.1. AngularJS

In Angular JS 1.x, the primary way to pass values into a directive is via HTML attributes. If you have ever dealt with the concept of isolated scope or use bindToController syntax, the code below is nothing new.

<!-- example.template.html -->
<example-directive
  one-way-binding="oneWay"
  two-way-binding="twoWay"
  text-binding="textBinding"
></example-directive>
/* example.directives.js */
(function() {
  var exampleDirective = function() {
    return {
      restrict: 'E',
      bindToController: {
        oneWayBinding: '&',
        twoWayBinding: '=',
        textBinding: '@'
      }
    };
  };
  exampleDirective.$inject = [];
  
  angular.module('app')
    .directive('exampleDirective', exampleDirective);
})();

There are 3 possible options for how we want tot pass values into a directive:

  • One-way binding (&): the param is passed into the directive and any changes inside the directive will not affect the param value outside of the context (or scope) of it.
  • Two-way binding (=): the param is passed into the directive and any changes made to the param value inside the directive will be reflected on the object outside of it.
  • Text binding (@): the param value is passed in as a text attribute.

1.2. Angular

With the @Input() metadata decorator in Angular version, values passed into HTML attributes are accessible inside the component locally by a variable matching the name of the attribute.

<!-- parent.component.html -->
<child-component
  [firstName]="firstName"
  [lastName]="lastName"
  [age]="age"
></child-component>
/* parent.component.ts */
import { Component } from '@angular/core';

@Component({
  templateUrl: './parent.component.html',
  ...
})
export class ParentComponent {
  firstName = 'Crimson';
  lastName = 'Dance';
  age = 24;
  
  ...
}
/* parent.component.ts */
import { Component } from '@angular/core';

@Component({
  selector: 'child-component',
  ...
})
export class ChildComponent {
  @Input() firstName: string;
  @Input() lastName: string;
  @Input() age: number;
  ...
}

Despite ending up being more straightforward, the interface is only one-way binding. Therefore, one very important thing that we need to keep in mind is that: a parent--child component relationship, any changes made to the @Input() variable inside the child will not be reflected in the parent component. In order to achieve such thing, we can establish two-way communication with the parent using @Output() decorator.

2. Output Bindings

When a component is added to the DOM, an instance of its class is instantiated. The class instance has an API that can be interacted with based on the access modifiers (such as public, private, protected) attached to the class’s variables and methods. Thus, if the class of our parent component is like below:

/* parent.component.ts */

export class ParentComponent {
  public firstName = 'Crimson';
  public lastName = 'Dance';
  private age = 24;
  
  public setAge(age: number) {
      this.age = age;
  }
  ...
}

as soon as an instance of ParentComponent is created, we will be able to access firstName, lastName and setAge() method, but not age in an external class, whether that is a service or another component, etc. This brings us to the @Output() decorator, which allows us to pass in a (public) method from a parent component down to a child component. From there, the child component can call the method when necessary. This is how two-way communication is established between a parent and child component.

For an example, let’s say we have a ParentComponent as a container and a ChildComponent which display the user data passed from the parent component as input. If we do something to interact with the user data, for example, incrementing user's age in the child component, you need to alert to the parent component about the change.

/* parent.component.ts */
...
export class ParentComponent {
  private age = 24;
  ...
  onAgeIncremented($event): void {
    this.age = $event;
  }
}
<!-- parent.component.html -->
<child-component
  [firstName]="firstName"
  [lastName]="lastName"
  [age]="age"
  (ageIncremented)="onAgeIncremented($event)"
></child-component>
/* child.component.ts */
import { Component, Input, Output, EventEmitter } from '@angular/core';
...
export class ChildComponent {
  @Input() firstName: string = '';
  @Input() lastName: string = '';
  @Input() age: number = 0;
  @Output() ageIncremented: EventEmitter<number> = new EventEmitter<number>();

  incrementAge(): void {
    this.age = this.age + 1;
    this.ageIncremented.emit(this.age);
  }
}
<!-- child.component.html -->
<span><strong>First Name</strong> {{firstName}}</span>
<span><strong>Last Name</strong> {{lastName}}</span>
<span><strong>Age</strong> {{age}}</span>
<button type="button" (click)="incrementAge()"></button>

Similar to the @Input() decorator, we match the @Output() variable name to the HTML attribute, then pass the method from the ParentComponent into the ChildComponent. The major difference from the @Input() variable declaration is how and what we’re instantiating it with. With an @Input() variable, you can choose to give it a default value or simply leave it as an empty placeholder (as we did above, '' for string type and 0 for number type). With an @Output(), you need to create a new instance of a built-in Angular class called EventEmitter, which is used by components to emit custom events.

Below is the explanation about the flow of data and event:

  • ParentComponent projects user data as input into ChildComponent.
  • In ChildComponent, the class retrieve input data according to their names declared respectively. Then user clicks on the button to has the @Input() age incremented, which trigger the function incrementAge.
  • In incrementAge function, we call this.ageIncremented.emit(this.age) to emit a custom as an output which alert the parent component about the data change. Due to the fact that we binded the output (ageIncremented)="onAgeIncremented($event)", it will invoke function onAgeIncremented in the parent component each time the event is emitted and the param $event passed is the value of age after incremented (modified) in the child component. Parent component is now aware of the change thank to accessing $event.

In conclusion, with the combination of the @Input() and @Output() decorators, this is reflective of the React.js mantra, in which

Events flow up, data flows down

Data is passed down by means of an @Input() decorator, while an event is emitted up through an @Output decorator.

In the next article, I will present you other 2 different mechanisms of communication between components: ViewChild and ContentChild.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí