DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

The joy of loosely coupled Angular components

Framework Training angular components header puffins talking image

Our JavaScript guru John has put together a nice little example that looks at how Angular components that communicate in a loosely coupled way can deal with change. This article looks at flexible approaches to component communication and we hope it sparks your imagination about how you might apply these methods in your own applications.

The DNA sequence application

  • This small app creates sequences of the four chemical bases in DNA: adenine (A), guanine (G), cytosine (C), and thymine (T).

  • The data is an array of objects containing the four bases.
const fourBases = [
    { name:"adenine", code:"A" },
    { name:"guanine", code:"G" },
    { name:"cytosine", code:"C" },
    { name:"thymine", code:"T" }
];
  • The app contains two components, base and draw which need to communicate in some way.
  • My knowledge of the science of genetics is zero. The content is purely a vehicle for exploring Angular.

The base

  • The base component randomly picks a base once a second
private base = null;

constructor() {
    interval( 1000 ).subscribe(n => this.setBase())
}

setBase() {
    let n = Math.floor(Math.random()*this.fourBases.length);
    this.base = this.fourBases[n];      
}
  • The template displays the name of the selected base.
  • The safe navigation operator (the question-mark suffix in base?.name) avoids a race-condition because property base is initially null.
<!-- Example base: { name:"adenine", code:"A" } -->
<section>{{ base?.name }}</section>

The sequence

  • The draw component displays a sequence of bases, iterating over the array using an ngFor directive.
<span *ngFor="let base of sequence" [ngClass]="base.name">{{ base.code }}</span>
  • The ngClass styling works because the CSS contains rules that match the base names.
.adenine{  background-color: blue; }
.guanine{  background-color: sienna; }
.cytosine{ background-color: orangered; }
.thymine{  background-color: forestgreen; }

Lets talk: Inputs & Outputs

  • The two components need to communicate.
  • Every second the base component picks a base which needs to be added to the sequence in the draw component.
  • If the two components share a common parent the default solution is to use Inputs and Outputs.
  • The base component defines and outputs a custom event.
@Output() changeBase: EventEmitter<any> = new EventEmitter();

this.changeBase.emit( this.base );
  • The parent template listens for changeBase events and calls a method in its own class to build the sequence.
<base (changeBase)="buildSequence($event)"></base>    
  • The buildSequence method adds the base to its sequence. The ES6 spread operator pushes a copy of the object to avoid copy-by-reference bugs.
  private sequence = [];

  buildSequence(base) {
    this.sequence.push({ ...base, code: base.code })
  }
  • The parent template passes the sequence into the draw component as an input.
  • The sequence array grows in length every second.
<draw [sequence]="sequence"></draw>
  • The draw component defines an Input.
@Input() dnaSequence;

Mediator

  • The code works in a loosely coupled way.
  • Base outputs events without knowing who is listening.
  • Draw inputs a growing sequence without knowing who its' parent is.
  • The two components share a common parent which acts as a mediator passing data between them in its template.
<base (changeBase)="buildSequence($event)"></base>
<draw [sequence]="sequence"></draw>

Service with a smile πŸ™‚

  • But what happens when we change the structure of the project? Without a common parent this approach stops working.
  • We could pass @Inputs down through intermediate components which pass them on to their descendants. However this could become verbose and inflexible as the project grows.
  • A better solution is to use a service as the mediator between the two components.
  • Services work in a loosely coupled way. Angulars' dependency injection (DI) instantiates and passes a service into each component via its constructor.
  • Here is a first attempt at a service which creates a sequence of bases.
export class MessageService {

  private sequence = [];


  addBase( base ) {
    this.sequence.push( {...base} );
  }

  getSequence() {
    return this.sequence;
  }
}
  • However, this service does not provide a way of notifying listeners that the sequence has changed.

Observable Subjects

  • An Observable Subject solves this problem. The base component can add values to the stream. The draw component is notified when this happens
  • This provides an ideal mechanism for passing messages around components in a loosely coupled way.
export class MessageService {

  private channel = new Subject<string>();

  constructor() {}

  sendMessage(s: any) {
    this.channel.next(s);
  }

  getChannel() {
    return this.channel.asObservable()
  }
}
  • The base component sends messages to the service.
constructor(private ms:MessageService) {}

setBase() {
    this.ms.sendMessage( this.base );
}
  • The draw component subscribes as a listener to the Observable stream and is notified of changes by the service .
constructor(private ms:MessageService) {}

setBase() {
     this.ms.getChannel().subscribe( base => this.sequence.push({...base}))
}
  • The service becomes the mediator between the two components.
  • All the Input and Output code can be removed.
  • The components do not need to share a common parent.
  • The code continues to work even if the project is refactored into a different component hierarchy.

Observable Clean-Up

  • Observables are not part of the Angular core. If the draw component instance is destroyed, the subscribe code will continue running causing a memory leak.
  • We need to store a reference to the Observable and then clean up in the onDestroy lifecycle method.
this.subs = this.ms.getChannel().subscribe( ....

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

Alternatives?

  • This article has compared the relative merits of Inputs/Outputs against a Service as a mediator between components. Are there other approaches worth considering?

Local Storage

  • The browser provides localStorage for reading and writing persistent domain-specific strings.
  • We could share the sequence of bases between components using localStorage.
// Base component
localStorage.bases = JSON.stringify( this.bases );

// Draw component
this.bases = localStorage.bases ? JSON.parse( localStorage.bases ) : [] ;
  • However this approach does not make use of the mechanisms provided by the Angular framework for component communication.
  • And localStorage can be easily deleted/corrupted by the end-user in mid-application from the browser console.
localStorage.bases = "rubbish";

Redux

  • Redux provides a store that the entire application can communicate with in a loosely coupled way.
  • The base component could dispatch an action when it chooses a new base.
export const BASE : string = "base" ;
export let setBase = ( b ) => ({ type: BASE, data: b }) ;
  • A reducer function handles the action and updates the Redux store.
  • The Redux store notifies listeners of any changes.
  • The draw component includes code to listen for store changes.
constructor(@Inject(BaseStore) private store:Redux.Store<Base>) {
this.store.subscribe(() => this.sequence = this.store.getState().sequence );
  • Like the service mediator, this approach avoids the need to wire up Inputs and Outputs, and creates a loosely coupled architecture.
  • Redux requires some additional setup and adds some complexity to the code.
  • But it does provide a useful set of debugging tools via the Chrome Redux Extension.

Summary

  • Components are the fundamental building blocks of Angular applications. Defining clear channels of communication between components is an essential part of that process.
  • My preference here would be to use a service as the mediator between two components. This provides a lightweight loosely coupled solution that will continue to work as the project changes.

A bit about us

Framework Training Ltd is a UK-based provider of face-to-face technical training. We have a fantastic family of domain experts, with coverage of key disciplines including software development in languages including JavaScript, C#, Python, Scala et al. We also specialise in DevOps platforms and techniques, Data Science, Agile methods and secure application development too. Here's our Angular training course outline to give you an idea of what we do.

We would love to know what you think about this tutorial and would welcome your questions.

Top comments (0)

Why You Need to Study Javascript Fundamentals

The harsh reality for JS Developers: If you don't study the fundamentals, you'll be just another β€œCoder”. Top learnings on how to get to the mid/senior level faster as a JavaScript developer by Dragos Nedelcu.