Templates and Data Binding

Module 14: JavaScript Frontend Frameworks - Vue & Angular

Introduction to Angular Templates

Templates in Angular are HTML files that tell Angular how to render the component's view. They are enhanced with Angular-specific syntax that allows for dynamic data binding, event handling, and template manipulation.

Real-world analogy: If a component is like a worker in a factory, the template is like the worker's instruction manual. The manual contains the standard procedures (HTML), but also special instructions that adapt based on the situation (Angular template syntax).

flowchart TD A[Angular Template] --> B[Standard HTML] A --> C[Interpolation {{data}}] A --> D[Data Binding [prop]="value"] A --> E[Event Binding (event)="handler()"] A --> F[Directives *ngIf, *ngFor] A --> G[Template Expressions] A --> H[Pipes | transform] style A fill:#DD0031,color:white style B fill:#C3002F,color:white style C fill:#C3002F,color:white style D fill:#C3002F,color:white style E fill:#C3002F,color:white style F fill:#C3002F,color:white style G fill:#C3002F,color:white style H fill:#C3002F,color:white

Template Syntax Overview

Angular extends HTML with additional syntax elements that enable dynamic viewing experiences. Let's explore these elements:

<!-- A simple Angular template with various syntax features -->
<div class="hero-profile">
  <!-- Interpolation -->
  <h2>{{hero.name | uppercase}} Details</h2>
  
  <!-- Property binding -->
  <img [src]="hero.imageUrl" [alt]="hero.name">
  
  <!-- Event binding -->
  <button (click)="saveHero()">Save</button>
  
  <!-- Two-way binding -->
  <input [(ngModel)]="hero.name" placeholder="name">
  
  <!-- Template reference variables -->
  <input #heroName placeholder="New hero name">
  <button (click)="addHero(heroName.value); heroName.value=''">Add</button>
  
  <!-- Built-in structural directives -->
  <div *ngIf="heroes.length > 0">There are {{heroes.length}} heroes.</div>
  
  <ul>
    <li *ngFor="let hero of heroes; let i = index; trackBy: trackByHeroId">
      {{i + 1}}. {{hero.name}}
      <button (click)="deleteHero(hero)">X</button>
    </li>
  </ul>
  
  <!-- ngSwitch directive -->
  <div [ngSwitch]="hero.type">
    <p *ngSwitchCase="'warrior'">Warrior specializes in combat.</p>
    <p *ngSwitchCase="'mage'">Mage specializes in spells.</p>
    <p *ngSwitchDefault>Unknown hero type.</p>
  </div>
  
  <!-- Attribute directives -->
  <p [ngClass]="{'special': isSpecial, 'danger': isDanger}">
    This paragraph has dynamic classes.
  </p>
  
  <p [ngStyle]="{'color': textColor, 'font-size.px': fontSize}">
    This paragraph has dynamic styles.
  </p>
  
  <!-- Pipes -->
  <p>Power level: {{hero.power | number:'1.1-2'}}</p>
  <p>Joined on: {{hero.joinedDate | date:'fullDate'}}</p>
  <p>Secret identity: {{hero.secretIdentity | lowercase}}</p>
</div>

This template showcases the core features of Angular's template syntax, which we'll explore in detail throughout this lecture.

Interpolation

Interpolation is the simplest form of data binding in Angular. It uses double curly braces {{}} to display a component property value in the view.

<!-- Basic interpolation -->
<h1>{{title}}</h1>
<p>My name is {{name}}</p>

<!-- Interpolation with expressions -->
<p>The sum of 1 + 1 is {{1 + 1}}</p>
<p>The hero's full name is {{hero.firstName + ' ' + hero.lastName}}</p>

<!-- Interpolation with method calls -->
<p>Capitalized name: {{getCapitalizedName()}}</p>

<!-- Interpolation with ternary operators -->
<p>Status: {{isActive ? 'Active' : 'Inactive'}}</p>

<!-- Interpolation in attributes (though property binding is preferred) -->
<div title="{{hero.name}} details">{{hero.name}}</div>

How Interpolation Works

flowchart LR A[Template with {{expression}}] --> B[Angular evaluates expression] B --> C[Converts result to string] C --> D[Updates the DOM text node] style A fill:#C3002F,color:white style B fill:#DD0031,color:white style C fill:#C3002F,color:white style D fill:#DD0031,color:white

Interpolation Limitations

Interpolation expressions cannot:

Real-world analogy: Interpolation is like a digital display that shows the current value from a sensor. The display doesn't affect the sensor; it only shows its value.

Property Binding

Property binding allows you to set a property of a DOM element to a value from a component. It uses square brackets [] around the target property.

<!-- Basic property binding -->
<img [src]="hero.imageUrl">
<button [disabled]="isDisabled">Save</button>

<!-- Property vs. attribute -->
<!-- Use property binding, not interpolation, for non-string properties -->
<input [value]="username">
<div [hidden]="isHidden">This may be hidden</div>

<!-- Class and style binding -->
<div [class.special]="isSpecial">Special div</div>
<div [style.color]="textColor">Colored text</div>
<div [style.width.px]="width">Sized div</div>

<!-- Binding to custom component properties -->
<app-hero-detail [hero]="selectedHero"></app-hero-detail>

One-way Data Flow

Property binding is one-way: from component to view. Changes in the component property value flow to the element property, but not vice versa.

flowchart LR A[Component Property] --> B[Property Binding] B --> C[DOM Element Property] style A fill:#DD0031,color:white style B fill:#C3002F,color:white style C fill:#C3002F,color:white

Property vs. Attribute Binding

It's important to understand the difference between HTML attributes and DOM properties:

<!-- Property binding (recommended) -->
<input [value]="username">

<!-- Attribute binding (for special cases) -->
<input [attr.aria-label]="ariaLabel">

When to use attribute binding: Use attribute binding when there is no corresponding DOM property for the attribute, such as ARIA attributes, SVG attributes, or custom data attributes.

Real-world analogy: If interpolation is like a display showing a value, property binding is like a dial or control that gets set to a specific position based on a remote signal.

Class and Style Binding

Angular provides specialized forms of property binding for manipulating an element's CSS classes and styles.

Class Binding

<!-- Single class binding -->
<div [class.active]="isActive">This is active</div>

<!-- Multiple class binding using ngClass -->
<div [ngClass]="currentClasses">This has multiple classes</div>

// In component
currentClasses = {
  'active': this.isActive,
  'disabled': !this.isEnabled,
  'special': this.isSpecial
};

<!-- Alternative ngClass syntax -->
<div [ngClass]="['bold', 'highlight']">This has array-based classes</div>
<div [ngClass]="getClassMap()">This uses a method to get classes</div>

Style Binding

<!-- Single style binding -->
<div [style.color]="textColor">This text has color</div>
<div [style.font-size.px]="fontSize">This text has size in pixels</div>
<div [style.width.%]="widthPercent">This div has percentage width</div>

<!-- Multiple style binding using ngStyle -->
<div [ngStyle]="currentStyles">This has multiple styles</div>

// In component
currentStyles = {
  'color': this.canSave ? 'green' : 'gray',
  'font-weight': this.isSpecial ? 'bold' : 'normal',
  'font-size.px': this.fontSize
};

<!-- Alternative ngStyle syntax -->
<div [ngStyle]="getStyleObject()">This uses a method to get styles</div>

Real-world analogy: Class and style binding are like outfit coordinators. Class binding is like choosing which pre-defined outfits (classes) to wear, while style binding is like adjusting individual elements of your outfit (color, size, etc.) on the fly.

Event Binding

Event binding allows you to listen for and respond to user actions such as keystrokes, mouse movements, clicks, and touches. It uses parentheses () around the target event.

<!-- Basic event binding -->
<button (click)="save()">Save</button>
<input (input)="handleInput($event)">
<div (mouseover)="showTooltip()" (mouseout)="hideTooltip()">Hover me</div>

<!-- Event object in the handler -->
<input (keyup)="onKeyUp($event)">

// In component
onKeyUp(event: KeyboardEvent) {
  if (event.key === 'Enter') {
    this.submit();
  }
  // The raw DOM event is available
  console.log(event.target);
}

<!-- Common DOM events -->
<button (click)="onClick()">Click</button>
<div (dblclick)="onDoubleClick()">Double-click</div>
<input (focus)="onFocus()" (blur)="onBlur()">
<div (mouseenter)="onMouseEnter()" (mouseleave)="onMouseLeave()">Mouse over/out</div>
<form (submit)="onSubmit()">...</form>
<input (keydown)="onKeyDown($event)" (keyup)="onKeyUp($event)">

Event Bubbling and Propagation

By default, events bubble up through the DOM tree. You can stop propagation if needed:

<!-- Stop event propagation -->
<div (click)="outerClick()">
  Outer div
  <button (click)="innerClick($event)">Click me</button>
</div>

// In component
innerClick(event: Event) {
  // Prevent the event from bubbling up to parent elements
  event.stopPropagation();
  // Handle inner click
  console.log('Inner clicked');
}

outerClick() {
  // This won't run when inner button is clicked
  // because propagation is stopped
  console.log('Outer clicked');
}

Event Modifiers

Angular doesn't have built-in event modifiers like Vue, but you can handle them in your event methods:

<!-- Preventing default behavior -->
<form (submit)="onSubmit($event)">
  <!-- form elements -->
  <button type="submit">Submit</button>
</form>

// In component
onSubmit(event: Event) {
  // Prevent the form from submitting the traditional way
  event.preventDefault();
  // Custom form handling
  this.submitForm();
}

Custom Events from Components

Components can emit custom events that parent components can listen for:

<!-- Parent component template -->
<app-hero-detail 
  [hero]="selectedHero" 
  (saved)="onHeroSaved($event)"
  (deleted)="onHeroDeleted($event)">
</app-hero-detail>

// Child component class
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'app-hero-detail',
  template: `
    <div>
      <h2>{{hero.name}} Details</h2>
      <button (click)="save()">Save</button>
      <button (click)="delete()">Delete</button>
    </div>
  `
})
export class HeroDetailComponent {
  @Input() hero: Hero;
  @Output() saved = new EventEmitter<Hero>();
  @Output() deleted = new EventEmitter<Hero>();
  
  save() {
    this.saved.emit(this.hero);
  }
  
  delete() {
    this.deleted.emit(this.hero);
  }
}

Real-world analogy: Event binding is like setting up various sensors and alarms. When something happens (button press, mouse movement), the alarm triggers and a specific response team (method) is activated to handle the situation.

Two-Way Data Binding

Two-way data binding combines property binding and event binding. It allows synchronizing data between component and view in both directions.

<!-- Two-way binding with ngModel -->
<input [(ngModel)]="name" placeholder="name">
<p>Hello, {{name}}!</p>

// To use ngModel, you need to import FormsModule in your module
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    FormsModule
  ],
  // other module properties
})
export class AppModule { }

How ngModel Works

The [(ngModel)] syntax is actually shorthand for a property binding and an event binding:

<!-- This: -->
<input [(ngModel)]="name">

<!-- Is the same as: -->
<input [ngModel]="name" (ngModelChange)="name = $event">
flowchart LR A[Component Property] --"[ngModel]"--> B[Input Element] B --"(ngModelChange)"--> A style A fill:#DD0031,color:white style B fill:#C3002F,color:white

Creating Custom Two-Way Binding

You can create custom components that support two-way binding using a property/event pattern:

// counter.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <button (click)="decrement()">-</button>
      <span>{{count}}</span>
      <button (click)="increment()">+</button>
    </div>
  `
})
export class CounterComponent {
  @Input() count: number = 0;
  @Output() countChange = new EventEmitter<number>();
  
  increment() {
    this.count++;
    this.countChange.emit(this.count);
  }
  
  decrement() {
    this.count--;
    this.countChange.emit(this.count);
  }
}

// Using the counter with two-way binding
<app-counter [(count)]="myCount"></app-counter>
<p>Current count: {{myCount}}</p>

Important: For custom two-way binding, the property name must match the event name with "Change" suffix (count and countChange).

Real-world analogy: Two-way binding is like a smart thermostat. It displays the current temperature (from model to view) and when you adjust it, it sends the new setting back to the heating system (from view to model).

Structural Directives

Structural directives change the DOM layout by adding, removing, or manipulating elements. They are recognizable by the asterisk (*) prefix.

*ngIf

The *ngIf directive adds or removes an element based on a condition:

<!-- Basic ngIf -->
<div *ngIf="isVisible">This element is sometimes shown.</div>

<!-- ngIf with else -->
<div *ngIf="heroes.length > 0; else noHeroes">
  There are {{heroes.length}} heroes.
</div>
<ng-template #noHeroes>
  <div>There are no heroes.</div>
</ng-template>

<!-- ngIf with then and else -->
<div *ngIf="heroes.length > 0; then heroList; else noHeroes"></div>
<ng-template #heroList>
  <div>There are {{heroes.length}} heroes.</div>
</ng-template>
<ng-template #noHeroes>
  <div>There are no heroes.</div>
</ng-template>

*ngFor

The *ngFor directive repeats an element for each item in an array:

<!-- Basic ngFor -->
<ul>
  <li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>

<!-- ngFor with index -->
<ul>
  <li *ngFor="let hero of heroes; let i = index">
    {{i + 1}}. {{hero.name}}
  </li>
</ul>

<!-- ngFor with additional variables -->
<ul>
  <li *ngFor="let hero of heroes; 
              let i = index; 
              let first = first; 
              let last = last; 
              let even = even; 
              let odd = odd"
      [class.first]="first"
      [class.last]="last"
      [class.even]="even"
      [class.odd]="odd">
    {{i + 1}}. {{hero.name}}
  </li>
</ul>

<!-- ngFor with trackBy (improves performance) -->
<ul>
  <li *ngFor="let hero of heroes; trackBy: trackByHeroId">
    {{hero.name}}
  </li>
</ul>

// In component
trackByHeroId(index: number, hero: Hero): number {
  return hero.id;
}

*ngSwitch

The *ngSwitch directives handle multiple possible values:

<div [ngSwitch]="hero.type">
  <div *ngSwitchCase="'warrior'">
    <app-warrior-detail [hero]="hero"></app-warrior-detail>
  </div>
  <div *ngSwitchCase="'mage'">
    <app-mage-detail [hero]="hero"></app-mage-detail>
  </div>
  <div *ngSwitchDefault>
    <app-unknown-hero-detail [hero]="hero"></app-unknown-hero-detail>
  </div>
</div>

How Structural Directives Work

The asterisk (*) syntax is shorthand for using <ng-template>:

<!-- This: -->
<div *ngIf="isVisible">Content to show</div>

<!-- Is the same as: -->
<ng-template [ngIf]="isVisible">
  <div>Content to show</div>
</ng-template>

<!-- And this: -->
<ul>
  <li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>

<!-- Is the same as: -->
<ul>
  <ng-template ngFor let-hero [ngForOf]="heroes">
    <li>{{hero.name}}</li>
  </ng-template>
</ul>

Real-world analogy: Structural directives are like construction foremen who decide which parts of a building to construct, how many times to repeat a section, or which of several possible designs to implement based on specifications.

Attribute Directives

Attribute directives change the appearance or behavior of an existing element. They look like regular HTML attributes.

Built-in Attribute Directives

ngClass

Adds or removes CSS classes:

<!-- With an object whose properties are class names -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}"></div>

<!-- With an array of class names -->
<div [ngClass]="['bold', 'highlight']"></div>

<!-- With a string of space-separated class names -->
<div [ngClass]="'bold highlight'"></div>

<!-- With a method that returns classes -->
<div [ngClass]="getClasses()"></div>

ngStyle

Sets inline styles:

<!-- With an object whose properties are style names -->
<div [ngStyle]="{'color': textColor, 'font-size.px': fontSize}"></div>

<!-- With a method that returns styles -->
<div [ngStyle]="getStyles()"></div>

Custom Attribute Directives

You can create your own attribute directives to encapsulate behavior:

// highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  @Input('appHighlight') highlightColor: string = 'yellow';
  @Input() defaultColor: string = 'transparent';
  
  constructor(private el: ElementRef) {
    this.setBackgroundColor(this.defaultColor);
  }
  
  @HostListener('mouseenter') onMouseEnter() {
    this.setBackgroundColor(this.highlightColor);
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.setBackgroundColor(this.defaultColor);
  }
  
  private setBackgroundColor(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

// Usage in a template
<p appHighlight="lightblue" defaultColor="#f0f0f0">
  Hover over me to see highlighting
</p>

Real-world analogy: Attribute directives are like special accessories that modify the behavior of an object without changing its core function. For example, adding a silencer to a car exhaust changes how it sounds without changing its ability to drive.

Template Reference Variables

Template reference variables provide direct access to an element, directive, or component from within a template. They are declared using the hash (#) symbol.

<!-- Basic reference variable -->
<input #nameInput placeholder="Name">
<button (click)="greet(nameInput.value)">Greet</button>

<!-- Reference variable with component -->
<app-counter #counter></app-counter>
<button (click)="counter.increment()">Increment Counter</button>

<!-- Reference variable with directive -->
<form #heroForm="ngForm" (ngSubmit)="submitForm(heroForm)">
  <input name="name" [(ngModel)]="hero.name" required #name="ngModel">
  <div *ngIf="name.invalid && name.touched">
    Name is required
  </div>
  <button type="submit" [disabled]="heroForm.invalid">Submit</button>
</form>

Scope: Template reference variables are only available within their template. They can't be accessed from other templates or from the component class directly.

Real-world analogy: Template reference variables are like name tags at a conference. They give you a convenient way to refer to and interact with specific people (elements) in a crowded room (template).

Template Expressions and Statements

Angular's template syntax uses expressions and statements to create dynamic content and handle events.

Template Expressions

Template expressions are used in interpolation and property binding:

<!-- Simple expressions -->
{{ username }}
{{ user.firstName + ' ' + user.lastName }}
{{ isActive ? 'Active' : 'Inactive' }}
{{ calculateTotal() }}

<!-- Expressions in property binding -->
[disabled]="isSubmitting || !isValid"
[class.active]="isActive && isEnabled"

Expression Guidelines:

Template Statements

Template statements are used in event binding:

<!-- Simple statements -->
(click)="save()"
(change)="username = $event.target.value"
(submit)="submitForm(); logSubmission()"

<!-- Statements with event information -->
(click)="deleteHero(hero, $event)"

Statement Guidelines:

Expression Context

Expressions in templates are evaluated within a specific context:

Real-world analogy: The expression context is like the environment a robot operates in. The robot can only interact with objects in its immediate surroundings (component properties) or objects that have been explicitly placed there (template variables).

Pipes

Pipes transform displayed values within templates. Angular provides several built-in pipes and allows you to create custom pipes.

Basic Pipe Syntax

<!-- Basic pipe -->
{{ value | pipeName }}

<!-- Pipe with parameters -->
{{ value | pipeName:param1:param2 }}

<!-- Chained pipes -->
{{ value | pipe1 | pipe2 }}

Built-in Pipes

<!-- String pipes -->
{{ name | uppercase }}  <!-- Converts to uppercase -->
{{ name | lowercase }}  <!-- Converts to lowercase -->
{{ name | titlecase }}  <!-- Capitalizes first letter of each word -->

<!-- Number pipes -->
{{ price | number:'1.2-2' }}  <!-- Format with 1 integer digit, 2-2 decimal digits -->
{{ price | currency }}  <!-- Format as currency ($123.45) -->
{{ price | currency:'EUR':'symbol':'1.2-2' }}  <!-- Format as Euro (€123.45) -->
{{ percentage | percent:'2.2-2' }}  <!-- Format as percentage (42.42%) -->

<!-- Date pipes -->
{{ birthdate | date }}  <!-- Format as date (Apr 15, 2019) -->
{{ birthdate | date:'shortDate' }}  <!-- Format as short date (4/15/19) -->
{{ birthdate | date:'fullDate' }}  <!-- Format as full date (Monday, April 15, 2019) -->
{{ birthdate | date:'yyyy-MM-dd' }}  <!-- Custom format (2019-04-15) -->

<!-- Other useful pipes -->
{{ object | json }}  <!-- Format as JSON string -->
{{ observable | async }}  <!-- Subscribes to observable and displays latest value -->
{{ array | slice:1:3 }}  <!-- Displays elements from index 1 to 2 -->

Custom Pipes

You can create your own pipes to encapsulate custom transformations:

// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 100, trail: string = '...'): string {
    if (!value) {
      return '';
    }
    
    if (value.length <= limit) {
      return value;
    }
    
    return value.substring(0, limit) + trail;
  }
}

// Register in a module
@NgModule({
  declarations: [
    TruncatePipe
  ],
  // other module properties
})
export class SharedModule { }

// Usage in a template
<p>{{ longText | truncate:50:'...' }}</p>

Pure vs. Impure Pipes

Pure Pipes (Default)

  • Executed only when input changes (by reference)
  • Better for performance
  • Use for transformations that don't depend on external state

Impure Pipes

  • Executed on every change detection cycle
  • Can be less performant
  • Necessary for transformations that depend on external state
@Pipe({
  name: 'filterHeroes',
  pure: false
})
export class FilterHeroesPipe implements PipeTransform {
  transform(heroes: Hero[], searchTerm: string): Hero[] {
    if (!heroes || !searchTerm) {
      return heroes;
    }
    
    return heroes.filter(hero => 
      hero.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }
}

Real-world analogy: Pipes are like food processors. They take raw ingredients (data) and transform them into more presentable or useful forms. Pure pipes are like simple appliances that process exactly what you put in, while impure pipes are like smart appliances that might adjust based on other factors like ambient temperature or humidity.

The Template vs. Display: Change Detection

Angular's change detection mechanism is responsible for keeping the view in sync with the component data.

How Change Detection Works

flowchart TB A[Event Trigger
UI Event, XHR, Timer] --> B[Zone.js
Intercepts Async Operations] B --> C[Angular Notified
of Potential Changes] C --> D[Change Detection
Runs] D --> E[Template Expressions
Re-evaluated] E --> F[DOM Updated
If Necessary] style A fill:#C3002F,color:white style B fill:#C3002F,color:white style C fill:#DD0031,color:white style D fill:#DD0031,color:white style E fill:#C3002F,color:white style F fill:#C3002F,color:white

By default, Angular uses a change detection strategy called "CheckAlways", which checks the entire component tree on every potential change.

OnPush Change Detection

For better performance, you can use the "OnPush" change detection strategy, which only checks the component when:

import { Component, ChangeDetectionStrategy, Input } from '@angular/core';

@Component({
  selector: 'app-hero-card',
  templateUrl: './hero-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeroCardComponent {
  @Input() hero: Hero;
  
  // This component will only update when:
  // 1. A new Hero object is passed to it (reference change)
  // 2. An event inside this component is triggered
  // 3. Change detection is manually triggered
}

Manual Change Detection

In some cases, you may need to manually trigger change detection:

import { Component, ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-data-display',
  templateUrl: './data-display.component.html'
})
export class DataDisplayComponent {
  data: any;
  
  constructor(private cdr: ChangeDetectorRef) { }
  
  loadData() {
    // Some operation that may not trigger change detection
    this.thirdPartyLibrary.getData().then(result => {
      this.data = result;
      // Manually trigger change detection
      this.cdr.detectChanges();
    });
  }
  
  // Other methods to manually control change detection:
  detach() {
    // Detach from change detection
    this.cdr.detach();
  }
  
  reattach() {
    // Reattach to change detection
    this.cdr.reattach();
  }
  
  markForCheck() {
    // Mark component to be checked (for OnPush)
    this.cdr.markForCheck();
  }
}

Real-world analogy: Change detection is like a security system that constantly monitors a building. The default strategy checks every room whenever any sensor is triggered, while OnPush is like a more efficient system that only checks specific areas based on precise triggers.

Practical Example: Reactive Form

Let's build a comprehensive example that demonstrates many of the template features we've learned about:

// user-profile.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserService } from '../services/user.service';
import { User } from '../models/user.model';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
  profileForm: FormGroup;
  user: User;
  isEditing = false;
  submitAttempted = false;
  saveSuccess = false;
  saveError = '';
  
  // Define form validation messages
  validationMessages = {
    name: {
      required: 'Name is required',
      minlength: 'Name must be at least 2 characters',
      maxlength: 'Name cannot exceed 50 characters'
    },
    email: {
      required: 'Email is required',
      email: 'Please enter a valid email address'
    },
    age: {
      min: 'Age must be at least 18',
      max: 'Age cannot exceed 120'
    }
  };
  
  // Available themes for the profile card
  themes = [
    { id: 'default', name: 'Default' },
    { id: 'dark', name: 'Dark' },
    { id: 'light', name: 'Light' },
    { id: 'professional', name: 'Professional' }
  ];
  
  constructor(
    private fb: FormBuilder,
    private userService: UserService
  ) { }
  
  ngOnInit(): void {
    // Initialize the form
    this.profileForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(50)]],
      email: ['', [Validators.required, Validators.email]],
      age: ['', [Validators.min(18), Validators.max(120)]],
      bio: [''],
      theme: ['default'],
      notifications: [true]
    });
    
    // Load user data
    this.loadUserData();
  }
  
  loadUserData() {
    this.userService.getCurrentUser().subscribe(
      (user: User) => {
        this.user = user;
        this.profileForm.patchValue({
          name: user.name,
          email: user.email,
          age: user.age,
          bio: user.bio || '',
          theme: user.theme || 'default',
          notifications: user.notifications
        });
      },
      error => {
        console.error('Failed to load user data', error);
      }
    );
  }
  
  toggleEdit() {
    this.isEditing = !this.isEditing;
    this.submitAttempted = false;
    this.saveSuccess = false;
    this.saveError = '';
    
    if (!this.isEditing) {
      // Reset form to original values when cancelling edit
      this.profileForm.patchValue({
        name: this.user.name,
        email: this.user.email,
        age: this.user.age,
        bio: this.user.bio || '',
        theme: this.user.theme || 'default',
        notifications: this.user.notifications
      });
    }
  }
  
  saveProfile() {
    this.submitAttempted = true;
    this.saveSuccess = false;
    this.saveError = '';
    
    if (this.profileForm.valid) {
      const updatedUser = {
        ...this.user,
        ...this.profileForm.value
      };
      
      this.userService.updateUser(updatedUser).subscribe(
        response => {
          this.user = updatedUser;
          this.saveSuccess = true;
          this.isEditing = false;
          setTimeout(() => this.saveSuccess = false, 3000);
        },
        error => {
          this.saveError = 'Failed to save profile. Please try again.';
          console.error('Error saving profile', error);
        }
      );
    } else {
      // Trigger validation for all fields
      Object.keys(this.profileForm.controls).forEach(key => {
        const control = this.profileForm.get(key);
        control.markAsTouched();
      });
    }
  }
  
  // Helper method to check for validation errors
  hasError(controlName: string, errorType: string): boolean {
    const control = this.profileForm.get(controlName);
    return control.touched && control.hasError(errorType);
  }
  
  // Get appropriate validation error message
  getErrorMessage(controlName: string): string {
    const control = this.profileForm.get(controlName);
    
    if (!control || !control.errors) {
      return '';
    }
    
    const messages = this.validationMessages[controlName];
    for (const key in control.errors) {
      if (messages && messages[key]) {
        return messages[key];
      }
    }
    
    return 'Invalid value';
  }
}

// user-profile.component.html
<div class="profile-container" [ngClass]="user?.theme || 'default'">
  <h2>User Profile</h2>
  
  <!-- Success message -->
  <div *ngIf="saveSuccess" class="alert success">
    Profile saved successfully!
  </div>
  
  <!-- Error message -->
  <div *ngIf="saveError" class="alert error">
    {{ saveError }}
  </div>
  
  <!-- Loading state -->
  <div *ngIf="!user" class="loading">
    Loading profile data...
  </div>
  
  <div *ngIf="user" class="profile-card">
    <!-- View mode -->
    <div *ngIf="!isEditing" class="profile-view">
      <div class="profile-header">
        <img [src]="user.avatarUrl || 'assets/default-avatar.png'" alt="User avatar">
        <h3>{{ user.name | titlecase }}</h3>
      </div>
      
      <div class="profile-details">
        <p>
          <strong>Email:</strong> {{ user.email }}
        </p>
        <p *ngIf="user.age">
          <strong>Age:</strong> {{ user.age }}
        </p>
        <p *ngIf="user.bio">
          <strong>Bio:</strong> {{ user.bio }}
        </p>
        <p>
          <strong>Theme:</strong> {{ (themes | filter:'id':user.theme)[0]?.name || 'Default' }}
        </p>
        <p>
          <strong>Notifications:</strong> {{ user.notifications ? 'Enabled' : 'Disabled' }}
        </p>
        <p>
          <strong>Member since:</strong> {{ user.joinDate | date:'mediumDate' }}
        </p>
      </div>
      
      <button (click)="toggleEdit()" class="btn btn-primary">Edit Profile</button>
    </div>
    
    <!-- Edit mode -->
    <div *ngIf="isEditing" class="profile-edit">
      <form [formGroup]="profileForm" (ngSubmit)="saveProfile()">
        <div class="form-group">
          <label for="name">Name</label>
          <input 
            type="text" 
            id="name" 
            formControlName="name"
            [class.invalid]="submitAttempted && profileForm.get('name').invalid">
          <div *ngIf="hasError('name', 'required') || hasError('name', 'minlength') || hasError('name', 'maxlength')" class="error-message">
            {{ getErrorMessage('name') }}
          </div>
        </div>
        
        <div class="form-group">
          <label for="email">Email</label>
          <input 
            type="email" 
            id="email" 
            formControlName="email"
            [class.invalid]="submitAttempted && profileForm.get('email').invalid">
          <div *ngIf="hasError('email', 'required') || hasError('email', 'email')" class="error-message">
            {{ getErrorMessage('email') }}
          </div>
        </div>
        
        <div class="form-group">
          <label for="age">Age</label>
          <input 
            type="number" 
            id="age" 
            formControlName="age"
            [class.invalid]="submitAttempted && profileForm.get('age').invalid">
          <div *ngIf="hasError('age', 'min') || hasError('age', 'max')" class="error-message">
            {{ getErrorMessage('age') }}
          </div>
        </div>
        
        <div class="form-group">
          <label for="bio">Bio</label>
          <textarea 
            id="bio" 
            formControlName="bio" 
            rows="4"></textarea>
        </div>
        
        <div class="form-group">
          <label for="theme">Theme</label>
          <select id="theme" formControlName="theme">
            <option *ngFor="let theme of themes" [value]="theme.id">
              {{ theme.name }}
            </option>
          </select>
        </div>
        
        <div class="form-group checkbox">
          <label>
            <input type="checkbox" formControlName="notifications">
            Enable notifications
          </label>
        </div>
        
        <div class="form-actions">
          <button type="button" (click)="toggleEdit()" class="btn btn-secondary">Cancel</button>
          <button type="submit" class="btn btn-primary" [disabled]="profileForm.invalid && submitAttempted">Save</button>
        </div>
      </form>
    </div>
  </div>
</div>

This example demonstrates:

Activities for Practice

Exercise 1: Data Binding Playground

Create a component that demonstrates all four types of data binding:

  • Interpolation to display various types of data
  • Property binding to manipulate element properties
  • Event binding to respond to user actions
  • Two-way binding to synchronize data

Include controls that allow users to change the data and see the effects in real-time.

Exercise 2: Dynamic Form Builder

Create a dynamic form builder component that:

  • Renders form fields based on a configuration object
  • Supports different input types (text, number, select, checkbox)
  • Implements validation with error messages
  • Uses ngIf, ngFor, and other directives to create the form structure
  • Collects and displays the form data when submitted

Exercise 3: Custom Pipe and Directive

Create a practical application that includes both a custom pipe and a custom directive:

  • Create a custom pipe that formats a phone number (e.g., "5551234567" to "(555) 123-4567")
  • Create a custom directive that validates an input as a valid phone number and provides visual feedback
  • Implement a simple contact form that uses both the pipe and directive
  • Add appropriate error handling and user feedback

Additional Resources