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).
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
Interpolation Limitations
Interpolation expressions cannot:
- Assign values to variables (
{{ x = 1 }}is not allowed) - Use
new,++,--, or similar operators - Use
if,for, or similar control statements - Use chaining expressions with
;or, - Interact with global namespace objects (
window,document, etc.)
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.
Property vs. Attribute Binding
It's important to understand the difference between HTML attributes and DOM properties:
- Attributes are defined in HTML and initialize DOM properties
- Properties are part of the DOM and can change
- Property binding sets DOM properties, not HTML attributes
<!-- 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">
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:
- Expressions should be quick to execute
- Expressions should have no visible side effects
- Expressions should be simple (move complex logic to the component)
- Expressions can't use JavaScript features like
new,++,--, etc.
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:
- Statements can include method calls, property assignments, and chained statements
- Statements can access the
$eventobject - Statements have access to the component's properties and methods
- Statements can't use JavaScript features like
new,++,--, etc.
Expression Context
Expressions in templates are evaluated within a specific context:
- The component instance is the primary context
- Template variables are also accessible in expressions
- For
*ngFor, loop variables likelet hero of heroesare accessible
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
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:
- An input property reference changes
- An event originated from the component or one of its children
- An observable bound with the async pipe emits a new value
- Change detection is manually triggered
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:
- Interpolation with expressions and pipes
- Property binding with class and style manipulation
- Event binding for form submission and UI interactions
- Structural directives (*ngIf, *ngFor) for conditional and list rendering
- Attribute directives (ngClass) for dynamic styling
- Template reference variables for accessing input values
- Form handling with validation
- Reactive form controls with dynamic error messages
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
- Angular Documentation: Template Syntax
- Angular Documentation: Pipes
- Angular Documentation: Structural Directives
- Angular Documentation: Attribute Directives
- Angular Documentation: Template Reference Variables
- Angular Documentation: Custom Pipes
- Angular Documentation: Change Detection
- Angular University: Reactive Templates
- In-Depth: Everything About Change Detection
- Angular Experts: Understanding Angular Templates