Sunday, January 21, 2018

Angular - Communication between components (Global notifications service)

Hi,

This post would focus on this case scenario:
An angular app has components which doesn't have a strong connection but still needs to communicate.
For instance, look at this app architecture:



















How the heck does component E can shout something to B and F or even make them trigger some actions?

1. Global Notifications Service



This solution is based on rxjs Subject.

Relevant notes about subjects:
1. Subject class implements both Observer and Observable interfaces which means senders & receivers shares the same subject instance. Subject is kind of a channel.

2. Unlike an Observable which can have only one observer (subscriber) - Subject has state and actually keeps a list of observers (Read more at: https://medium.com/@benlesh/on-the-subject-of-subjects-in-rxjs-2b08b7198b93). 

3. next function - Sends message to the channel.
subscribe function - Registers a handler function which executes after a message received.
unsubscribe function - Unregister current subscriber.

4. In this sample a regular Subject is used which means it's very important to subscribe before next is executed (Read more at: https://stackoverflow.com/questions/36814995/rxjs-multiple-subscriptions-to-observable).

5. Always remember to unsubscribe & do not subscribe (accidentally) more than once.

Plunker explanation

Here is a brief on what's going on between these app components:

  • Second & Third component subscribed to First component messages (subscription is taken place in the constructor).
  • Third component subscribed to Fourth component messages.
  • Third component has an option to unsubscribe from First component messages and an option to subscribe again.


@Injectable()
export class GlobalNotifications {
  private nameToSubject: Map<string, Subject> = new Map<string, Subject>();

This solution is based on Map type (key-value store). nameToValue key represents the notification name (= channel name) and value is the subject created for it.

getSubject(name: any) : Subject {
    let subjectObj : Subject = this.nameToSubject.get(name);
    if(!subjectObj) {
      subjectObj = new Subject();
      this.nameToSubject.set(name,subjectObj);
    }
    
    return subjectObj;
  }

getSubject simply returns a subject by name from this store. If a subject doesn't exist - this method would also create one.

export class SecondComponent implements OnDestroy {
    private dataReceived : Array<string> = [];
    private subscriberFirst : Subscriber;
    
  constructor(private _globalNot:GlobalNotifications) {
    this.subscriberFirst = this._globalNot.getSubject('firstToSubs').subscribe((val) => this.dataReceived.push(val));
  }

Second component gets GlobalNotifications service instance from angular injector.
It executes getSubject with a relevant channel name ('firstToSubs'), gets the desired Subject instance and subscribes to its messages with a method.
subscribe method returns Subscriber instance which is important to save as a class member as it would be used to unsubscribe.


ngOnDestroy() {
    this.subscriberFirst.unsubscribe();
  }

In most cases - when a component destroyed it's implies that unsubscribe is required. You DON'T want to a subscriptions hell.


export class FirstComponent {
  constructor(private _globalNot:GlobalNotifications) {
  }
  
  onClick() {
  let subjectObj : Subject = this._globalNot.getSubject('firstToSubs');
   subjectObj.next("Sent from First Component");
  }
}

First component sends messages over 'firstToSubs' subject by getting its instance (getSubject) and using next method on it.

If you would click the Second component Unsubscribe from first button, First component messages would arrive only to the Fourth component.

Notes:

1. This post isn't just about sharing data (or just sending messages). It's possible to use it in order to trigger different methods on different components when something occurs.

2. This solution offers an async one-way invoke with no response. In order to response back to caller - another subject has to be defined.

3. If an app uses ngrx\store library - it's possible to use store as a similar solution (https://auth0.com/blog/managing-state-in-angular-with-ngrx-store/).
Plunker: https://embed.plnkr.co/cr4rCJ0hRVMwuLzKe4mg/

2. Bubbling events and using ngOnChanges



This solution uses an event that bubbles from a child to parent. Parent notifies children by changing an @Input property.

Plunker explanation

  • First component sends message to Second component

@Component({
  selector: 'first-component',
  template: '<h1>First Component (Sender)</h1><button (click)="onClick()">Send to components</button>'
})
export class FirstComponent {
@Output('messageSent') messageSent = new EventEmitter<string>();
  
  onClick() {
    this.messageSent.emit("Sent from First Component");
  }
}

First component is extremely simple. onClick simply emits a message (component output).

Note: it's impossible to use @Output or EventEmitter inside a typescript class or service.
In this case, this Observer & Observable pattern can be used: https://embed.plnkr.co/eDDlFsMfWYcP24RFOKtA/


@Component({
  selector: 'my-app',
  template: `
    <first-component (messageSent)="onMessageSentFromFirst($event)"></first-component>
    <br/>
    <second-component [dataReceived]="messages" [isChanged]="isChanged"></second-component>
    <br/>
  `,
})
export class App {
  public messages:any = [];
  private isChanged:boolean;
  
  public onMessageSentFromFirst(event : any) {
    this.messages.push(event);
    this.isChanged = !this.isChanged;
  }
}

App component is First and Second components parent and here is how data flows:
1. (messageSent) is the event fired from first-component - onMessageSentFromFirst is triggered.
2. Inside onMessageSentFromFirst the message (event) is pushed into messages array.
3. messages array is assigned to [dataReceived] (an input of second-component).
4. isChanged is used to notify second-component a change has been made to messages array.

@Component({
  selector: 'second-component',
  template: '<h1>Second Component (Receiver)</h1><div *ngFor="let data of dataReceived">{{data}}</div>'
})
export class SecondComponent {
  @Input()
    private dataReceived : Array<string>;
    private dataReceivedLength = 0;
    @Input()
    public isChanged:boolean;

  
  ngOnChanges(data:any) {
    if(data.isChanged) {
      this.onMessageReceived(this.dataReceived[this.dataReceived.length-1]);
    }  
  }
  
  ngDoCheck() {
    if(this.dataReceived.length != this.dataReceivedLength) {
      this.dataReceivedLength = this.dataReceived.length;
      this.onMessageReceived(this.dataReceived[this.dataReceived.length-1]);
    }
  }
  
  public onMessageReceived(message : string) {
   // Perform actions
  }
}

Second component always displays dataReceived array content.

There are two ways to trigger a change in dataReceived array:
1. ngOnChanges triggers only when an input reference is changed. In this case, dataReceived array reference stays exactly as before (just with one more item added to the array). By adding another boolean @Input (isChangedApp component notify a change has been made and ngOnChanges gets triggered.

2. By using ngDoCheck it's possible to check for change without the need of outer intervention. Simply by compare previous and current dataReceived length. On performance matters, ngDoCheck gets called in a very high-frequency so it should be efficient.

Notes:

1. dataReceived reference equals to the messages reference sent from app component - that means any change in it would affect both arrays.

2. Parent can also notify child by grabbing it's instance (using @ViewChild) and invoke one of his public methods.
For more: https://angular-2-training-book.rangle.io/handout/advanced-components/access_child_components.html

3. emit default is synchronous invocation.

3. Connect components via service for a regular request-response (synchronous) invocation



This solution uses a service to expose one component to another.

Plunker explanation


interface IReceiver {
  receiveMessage: (message:string) => string
};

To avoid tight-coupling - service would a reference to IReceiver (a reference to a component which would receive a message).


@Injectable()
export class Communicator {
  public receiver : IReceiver;
  
}

Service just holds a reference.
This could be a general service that holds several references by using a map structure (similar to the service presented in the first solution presented in this post).


export class SecondComponent implements IReceiver {
  private message: string;
  
  constructor(private _communicator:Communicator) {
    _communicator.receiver = this;
  }

  public receiveMessage(message:string) : string {
    this.message = message;
    return message + " Received";
  }
}

Second component implements IReceiver and registers as a receiver in the constructor.


export class FirstComponent implements OnInit {
  message: string;
  status: string;
  
  constructor(private _communicator:Communicator) {
  }
  
  onClick() {
    this.status = this._communicator.receiver.receiveMessage(this.message);
  }
}

First component easily invokes receiveMessage method synchronically and receives an immediate response.

Summary


This post presents various ways in order to communicate between siblings components.
Choosing which one to use depends on case scenario.
For instance: If two siblings are hierarchically far from each other - using the second method can be pretty annoying (you would have to bubble up the event again and again and then sink it many levels down).

For more helpful information visit Angular official site:

Thank you Blogger, hello Medium

Hey guys, I've been writing in Blogger for almost 10 years this is a time to move on. I'm happy to announce my new blog at Med...