Angular Journey: Part 2 - Bindings, Directives, Components, Services

Zaki Mohammed Zaki Mohammed
Nov 09, 2024 | 7 min read | 125 Views | Comments

Healthy communication and maintaining strong binding are key in any relationship. Same goes in any Angular as well. In this article, we will explore the ways Angular offers to form bindings between templates and code-behind, along with communication between components.

Writing articles with bare hands is a tough job, but worth if it helps others. Keeping that in mind, let us target the goal of this article. Angular is completely component driven with the idea of dividing UI elements, pages and screens into smaller components and dealing with them individually, respecting SRP. Some primary Angular concepts are briefed below, which we will explore practically with our Notesy app continued from previous article:

  • Bindings: Helps to connect template (.html) logic with the TypeScript code logic.
    • One Way Bindings (Interpolation)
    • Property Bindings
    • Even Bindings
    • Two Way Bindings (ngModel)
  • Directives: Provides additional behavior to the elements. Directives are of 2 types, in built directives (*ngIf, *ngFor, etc.) and custom-made directives.
  • Component Communication: Provides a way to interact between components. Parent-Child, sibling or global component communications.

NOTE: We will showcase only the snippets of code in this article to keep it short and crips. If you want to view the entire code base feel free to clone the repository and view the entire file, the links are given at the bottom of the article.

We will now talk in the context of Notesy App and understand the above-mentioned concepts, lets proceed with below pointers:

  • Exploring Directives
  • Component Communication (Parent-Child: Input)
  • One Way and Property Bindings
  • Component Communication (Services)
  • Event Bindings
  • Two Way Bindings
  • Component Communication (Parent-Child: Output)
  • Validation and Error Handling

Exploring Directives

If we recall where we left in the previous article, we had a list component which looks like this, list.component.html:

<app-item></app-item>
<app-item></app-item>
<app-item></app-item>

Pretty ugly, but let's add life to this. Instead of manually repeating the component, we will iterate through an in-built Angular directive called *ngFor, list.component.html:

<div *ngFor="let note of [1, 2, 3]">
    <app-item></app-item>
</div>

Here, what we did is simply wrapped the item component within a div and added *ngFor directive. The syntax looks like:

*ngFor="let variable_name of array_name"

Here, the variable name can be anything, and the array can be defined in the TypeScript file. In previous code we just created an array on fly with 3 numbers, which makes the loop iterates for 3 times.

We will create a notes data file from which we will iterate actual notes, for this will add a data file in the app folder named as data.ts file, along with a note.model.ts file:

export interface Note {
  id: string;
  content: string;
}

The data.ts file:

import { Note } from "./models/note.model";

export const notes: Note[] = [
  {
    id: '67b15a30-df51-4e0f-8cc3-b061bdcc12ee',
    content: 'Consectetur labore est amet aute pariatur enim in sint pariatur ipsum culpa.',
  },
  {
    id: '4496fcdc-e3fe-46e1-9c58-e8f83ef1f587',
    content: 'Et occaecat amet duis anim.',
  },
  {
    id: 'cc2826d0-6939-42a1-a8aa-3639cb38eb5a',
    content: 'Voluptate voluptate cupidatat ut eiusmod Lorem consequat commodo.',
  },
];

Let's consume this array in our list component .ts:

import { notes as notesData } from '../data';

@Component({...});
export class ListComponent {
  notes = notesData;
}

Updating the list component .html:

<div *ngFor="let note of notes">
    <app-item></app-item>
</div>

This makes the notes card repeats 3 times.

But what if we don't have any notes list to show, in that case we must show the empty component. For now let's add the notes data file to empty component just how we did for the list component:

import { notes as notesData } from '../data';

@Component({...});
export class EmptyComponent {
  notes = notesData;
}

Now, in the empty component .html file we will make the empty message to appear conditionally, in case if the notes length is zero then we show the message, otherwise won't, empty.component.html:

 <div *ngIf="notes.length === 0" class="text-center p-3 text-muted">
  <h1 class="display-4 text-secondary">
    <i class="bi bi-egg me-2"></i>
  </h1>
  No notes found
</div>

Here, we have added the *ngIf Angular's inbuilt directive, which help us to write such conditional logics.

Component Communication (Parent-Child: Input)

The item component is not showing the correct data so far. Let's make it dynamic to have the supplied note object from the list component. Here, we are now talking about communication between a parent component (list) and a child component (item). To supply values to the child component we have to create a property in the child component with a decorator (@Input), item.component.html:

import { Component, Input } from '@angular/core';
import { Note } from '../../models/note.model';

@Component({...})
export class ItemComponent {
  @Input() note: Note;
}

Here, we have created a note property of type Note with @Input decorator. It's time to supply the note input to item component from parent component, list.component.html:

<div *ngFor="let note of notes">
    <app-item [note]="note"></app-item>
</div>

Here, we are doing a property binding using the square brackets with the same name as input present in the item component.

Additionally, we can make the input parameter required which as well provide error on consumption of the child component anywhere in any future parent component. To do so we update the @Input decorator in the item.component.ts:

import { Component, Input } from '@angular/core';
import { Note } from '../../models/note.model';

@Component({...})
export class ItemComponent {
  @Input({ required: true }) note!: Note;
}

Here, we have applied a not operator on note property stating it's not nullable.

One Way and Property Bindings

This makes the note value to reach to the item component, but it is still not in consumption. Will update the item.component.html file:

<div class="card bg-body-secondary border-0 mb-2">
  <div class="card-body">
    <p class="card-text">
      <span class="d-flex justify-content-between align-items-center">
        <span>{{note.content}}</span>
        <button type="button" class="btn btn-light ms-3" title="Remove">
          <i class="bi bi-trash-fill text-danger fs-6"></i>
        </button>
      </span>
    </p>
  </div>
</div>

Here, in order to consume the property value in the .html template file which is called as one way binding (interpolation) we use the 2 curly braces syntax ({{property}}).

The property we have already seen previously in the list.component.html file:

<app-item [note]="note"></app-item>

Component Communication (Services)

Coming back to the data.ts file, which is currently a static data source and won't be there in near future of this article series. To come closer to real world scenario, let us create a service to share data between components those are beyond parent-child relationship. To do so in Angular we have concept called services. Services are injectable classes those are globally accessible and has multiple usage scenarios in Angular. For now, narrowing down the usage of service as a sharable unit between components. We will see more usage of services in this Angular journey series. To create a service, we run below command in a new "services" folder:

ng g s note

We get a service shown below, note.service.ts:

import { Injectable } from '@angular/core';
import { notes as notesData } from '../data';

@Injectable({
  providedIn: 'root'
})
export class NoteService {
  notes = notesData;

  constructor() { }
}

Here, we have added the notes data from the data.ts file. Let us now consume this in the list.component.ts file:

import { Component } from '@angular/core';
import { Note } from '../../models/note.model';
import { NoteService } from '../../services/note.service';

@Component({...})
export class ListComponent {
  constructor(private noteService: NoteService) {}

  get notes() {
    return this.noteService.notes;
  }

  set notes(value: Note[]) {
    this.noteService.notes = value;
  }
}

Here, we have injected the NoteService in the constructor of the list component. We also created a getter and setter for the same for future usage.

Let's not consume the empty component and add the note service instead of the data.ts file code to empty.component.ts file:

import { Component } from '@angular/core';
import { NoteService } from '../../services/note.service';

@Component({...})
export class EmptyComponent {
  constructor(private noteService: NoteService) {}

  get notes() {
    return this.noteService.notes;
  }
}

This brings our attention to the form component now, let us add the note service to the form component as well, as the usage will be there in the form.component.ts file:

import { Component } from '@angular/core';
import { NoteService } from '../../services/note.service';

@Component({...})
export class FormComponent {
  constructor(private noteService: NoteService) {}
}

Event Binding

In the form component we are now adding a click event to the add button, the form.component.html looks like:

<form>
  <div class="input-group mb-4">
    <input type="text" class="form-control" placeholder="Write some notes..." />
    <button class="btn btn-success" type="submit" title="add" (click)="onAdd($event)">
      <i class="bi bi-plus-lg fs-5 fw-bolder"></i>
    </button>
  </div>
</form>

Adding necessary "onAdd" method in the form.component.ts file:

import { Component } from '@angular/core';
import { NoteService } from '../../services/note.service';
import { Note } from '../../models/note.model';

@Component({...})
export class FormComponent {
  constructor(private noteService: NoteService) {}

  onAdd(event: Event) {
    const newNote: Note = {
      id: 1000,
      content: 'New Note',
    };

    this.noteService.notes.push(newNote);
  }
}

Here, we are adding the new note to the notes service property.

Two Way Bindings

This is cool but not enough, let's do something about the id and the content. For the id we will use uuid NPM package to get always new id. Run below command for uuid:

npm i uuid

Using the uuid for the new note as below:

import { Component } from '@angular/core';
import { NoteService } from '../../services/note.service';
import { Note } from '../../models/note.model';
import { v4 as uuid } from 'uuid';

@Component({...})
export class FormComponent {
  constructor(private noteService: NoteService) {}

  onAdd(event: Event) {
    const newNote: Note = {
      id: uuid(),
      content: 'New Note',
    };

    this.noteService.notes.push(newNote);
  }
}

To get the actual note content entered from the input text we will use the two-way binding syntax of Angular using ngModel directive, in the form.component.html:

<form>
  <div class="input-group mb-4">
    <input type="text" class="form-control" placeholder="Write some notes..." [(ngModel)]="content" name="content" />
    <button class="btn btn-success" type="submit" title="add" (click)="onAdd($event)">
      <i class="bi bi-plus-lg fs-5 fw-bolder"></i>
    </button>
  </div>
</form>

But we need a property named content as well in the form.component.ts:

import { Component } from '@angular/core';
import { NoteService } from '../../services/note.service';
import { Note } from '../../models/note.model';
import { v4 as uuid } from 'uuid';

@Component({...})
export class FormComponent {
  content: string = '';
  
  constructor(private noteService: NoteService) {}

  onAdd(event: Event) {
    const newNote: Note = {
      id: uuid(),
      content: this.content,
    };

    this.noteService.notes.push(newNote);
  }
}

Component Communication (Parent-Child: Output)

Last but not least, let us work on the removal of the notes by clicking on the remove button of the item.component.html:

<div class="card bg-body-secondary border-0 mb-2">
  <div class="card-body">
    <p class="card-text">
      <span class="d-flex justify-content-between align-items-center">
        <span>{{note.content}}</span>
        <button type="button" class="btn btn-light ms-3" (click)="remove()" title="Remove">
          <i class="bi bi-trash-fill text-danger fs-6"></i>
        </button>
      </span>
    </p>
  </div>
</div>

Here, we have added the remove method and bind it to the click event. Creating the remove method in the form.component.ts file:

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Note } from '../../models/note.model';

@Component({...})
export class ItemComponent {
  @Input({ required: true }) note!: Note;
  @Output() onRemove = new EventEmitter<Note>();

  remove() {
    this.onRemove.emit(this.note);
  }
}

Here, we have added the @Output decorator for the onRemove event emitter. When child component wants to inform parent regarding event occurrence then we EventEmitter class which further can be consumed in the list.component.html:

<div *ngFor="let note of notes">
    <app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
</div>

In the list.component.ts file:

import { Component } from '@angular/core';
import { Note } from '../../models/note.model';
import { NoteService } from '../../services/note.service';

@Component({...})
export class ListComponent {
  onRemove(note: Note) {
    this.notes = this.notes.filter((n) => n.id !== note.id);
  }
}

Here, the onRemove method of list component will be triggered from the child component remove event.

Validation and Error Handling

Let us add the basic validation logic in the onAdd method, form.component.ts:

onAdd(event: Event) {
  event.preventDefault();

  if (!this.content) {
    alert('Please fill the form');
    return;
  }

  const newNote: Note = {
    id: uuid(),
    content: this.content,
  };

  this.noteService.notes.push(newNote);
}

We have also added the event.preventDefault() as we are inside the form element and to avoid the post back this is required, rest remains the basic validation logic.

Adding now the try-catch to be resilient and error free coding:

onAdd(event: Event) {
  try {
    event.preventDefault();

    if (!this.content) {
      alert('Please fill the form');
      return;
    }

    const newNote: Note = {
      id: uuid(),
      content: this.content,
    };

    this.noteService.notes.push(newNote);
    
  } catch (error: any) {
    alert(`Error Occurred: ${error.message}`);
  } finally {
    this.content = '';
  }
}

Lastly, to show new notes appear on top we just make a basic modification in the *ngFor notes array:

<div *ngFor="let note of notes.reverse()">
    <app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
</div>

Here, we have added the reverse() method of array to show the latest note card on the top.


Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.