Angular Architecture
Introduction
Angular is a platform and framework for building client-side applications with HTML and TypeScript. The architecture of an Angular application relies on a few fundamental concepts: Modules, Components, Templates, Metadata, Data Binding, Directives, Services, and Dependency Injection. Understanding these concepts and how they fit together is crucial for developing Angular applications effectively.
In this lesson, we'll explore Angular's architecture in detail, examining each building block and how they interact to form a complete application.
Angular Building Blocks
Modules
At the core of every Angular application is at least one NgModule - the root module, conventionally named AppModule
. Modules help organize an application into cohesive blocks of functionality.
Let's look at a basic module:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
This is what each part means:
- declarations: Components, directives, and pipes that belong to this module
- imports: Other modules whose exported classes are needed by component templates in this module
- providers: Services that this module contributes to the global collection of services
- bootstrap: The main application view (root component) that hosts all other app views
Components
Components are the building blocks of the user interface. Each component controls a portion of the screen (a view) through its associated template and class.
Here's a simple component:
import { Component } from '@angular/core';
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent {
heroes: string[] = ['Iron Man', 'Captain America', 'Thor', 'Hulk'];
selectedHero: string = '';
selectHero(hero: string): void {
this.selectedHero = hero;
}
}
And its template:
<h2>Hero List</h2>
<ul>
<li *ngFor="let hero of heroes" (click)="selectHero(hero)">
{{ hero }}
</li>
</ul>
<p *ngIf="selectedHero">You selected: {{ selectedHero }}</p>
When rendered, this creates a list of heroes that you can click to select.
Templates
Templates are HTML with Angular-specific syntax that tell Angular how to render the component. Templates use data binding to coordinate the application and DOM data, directives to apply application logic to what is displayed, and pipes to transform data before it's displayed.
Key template features include:
- Interpolation:
{{ value }}
- Property binding:
[property]="value"
- Event binding:
(event)="handler()"
- Two-way binding:
[(ngModel)]="property"
Services and Dependency Injection
Services are a way to share functionality across components. A service is typically a class with a specific purpose. Here's a basic service:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class HeroService {
private heroes: string[] = ['Iron Man', 'Captain America', 'Thor', 'Hulk'];
getHeroes(): string[] {
return this.heroes;
}
addHero(hero: string): void {
this.heroes.push(hero);
}
}
You can inject this service into components using Angular's dependency injection system:
import { Component, OnInit } from '@angular/core';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {
heroes: string[] = [];
selectedHero: string = '';
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.heroes = this.heroService.getHeroes();
}
selectHero(hero: string): void {
this.selectedHero = hero;
}
addHero(heroName: string): void {
this.heroService.addHero(heroName);
this.heroes = this.heroService.getHeroes(); // Get the updated list
}
}
Angular Application Structure
A typical Angular application has the following directory structure:
my-app/
├── node_modules/
├── src/
│ ├── app/
│ │ ├── components/
│ │ ├── services/
│ │ ├── models/
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ ├── app.component.css
│ │ ├── app.module.ts
│ ├── assets/
│ ├── environments/
│ ├── index.html
│ ├── main.ts
│ ├── styles.css
├── angular.json
├── package.json
├── tsconfig.json
- src/app: Contains the components, services, and other code files
- src/assets: Contains static files like images
- src/environments: Contains environment-specific configuration
- src/index.html: The main HTML page
- src/main.ts: The main entry point that bootstraps the application
- angular.json: Angular workspace configuration
- package.json: NPM package dependencies
- tsconfig.json: TypeScript compiler configuration
Data Flow in Angular
Angular architecture promotes a unidirectional data flow, which means the data flows from parent components to child components. This approach makes applications more predictable and easier to debug.
Input and Output Properties
For parent-child component communication, Angular provides:
- @Input() decorators to pass data from parent to child
- @Output() decorators with EventEmitter to emit events from child to parent
Example:
Parent component (parent.component.ts):
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `
<h1>Parent Component</h1>
<app-child
[message]="parentMessage"
(messageEvent)="receiveMessage($event)">
</app-child>
<p>Message from child: {{ messageFromChild }}</p>
`
})
export class ParentComponent {
parentMessage: string = 'Hello from parent';
messageFromChild: string = '';
receiveMessage(message: string) {
this.messageFromChild = message;
}
}
Child component (child.component.ts):
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
template: `
<h2>Child Component</h2>
<p>Message from parent: {{ message }}</p>
<button (click)="sendMessage()">Send Message to Parent</button>
`
})
export class ChildComponent {
@Input() message: string = '';
@Output() messageEvent = new EventEmitter<string>();
sendMessage() {
this.messageEvent.emit('Hello from child');
}
}
Real-World Application Example
Let's build a simple task management application to illustrate Angular's architecture:
- First, we need a Task interface:
// task.model.ts
export interface Task {
id: number;
title: string;
completed: boolean;
}
- Next, a service to manage tasks:
// task.service.ts
import { Injectable } from '@angular/core';
import { Task } from './task.model';
@Injectable({
providedIn: 'root'
})
export class TaskService {
private tasks: Task[] = [
{ id: 1, title: 'Learn Angular', completed: false },
{ id: 2, title: 'Build a project', completed: false },
{ id: 3, title: 'Deploy application', completed: false }
];
getTasks(): Task[] {
return this.tasks;
}
addTask(title: string): void {
const newTask: Task = {
id: this.tasks.length + 1,
title,
completed: false
};
this.tasks.push(newTask);
}
toggleTaskCompletion(id: number): void {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
}
}
}
- A component to display the task list:
// task-list.component.ts
import { Component, OnInit } from '@angular/core';
import { TaskService } from '../task.service';
import { Task } from '../task.model';
@Component({
selector: 'app-task-list',
template: `
<div class="task-list">
<h2>My Tasks</h2>
<div class="task-form">
<input [(ngModel)]="newTaskTitle" placeholder="Add new task" />
<button (click)="addTask()">Add</button>
</div>
<div class="tasks">
<div *ngFor="let task of tasks" class="task-item">
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleTaskCompletion(task.id)"
/>
<span [class.completed]="task.completed">{{ task.title }}</span>
</div>
</div>
<div class="task-stats">
<p>{{ completedTasksCount }} of {{ tasks.length }} tasks completed</p>
</div>
</div>
`,
styles: [`
.task-list {
width: 400px;
margin: 0 auto;
}
.task-form {
display: flex;
margin-bottom: 20px;
}
.task-form input {
flex-grow: 1;
padding: 8px;
}
.task-form button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
}
.task-item {
padding: 8px;
border-bottom: 1px solid #ddd;
}
.completed {
text-decoration: line-through;
color: #888;
}
`]
})
export class TaskListComponent implements OnInit {
tasks: Task[] = [];
newTaskTitle: string = '';
constructor(private taskService: TaskService) {}
ngOnInit(): void {
this.tasks = this.taskService.getTasks();
}
addTask(): void {
if (this.newTaskTitle.trim()) {
this.taskService.addTask(this.newTaskTitle);
this.newTaskTitle = '';
}
}
toggleTaskCompletion(id: number): void {
this.taskService.toggleTaskCompletion(id);
}
get completedTasksCount(): number {
return this.tasks.filter(task => task.completed).length;
}
}
- Finally, we need to declare this in our module:
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { TaskListComponent } from './tasks/task-list.component';
@NgModule({
declarations: [
AppComponent,
TaskListComponent
],
imports: [
BrowserModule,
FormsModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
This simple task application demonstrates:
- Components for displaying UI
- Services for business logic and data handling
- Models for type definitions
- Data binding for user interactions
- Dependency injection for providing services to components
Summary
Angular's architecture is designed around these key principles:
- Modularity: Breaking down applications into feature modules
- Component-based UI: Reusable UI components with their own logic
- Services and Dependency Injection: Sharing functionality across components
- Templates with Data-Binding: Displaying data and responding to user events
- Directives: Extending HTML with custom attributes and elements
Understanding these concepts gives you a solid foundation to build Angular applications of any size and complexity.
Additional Resources
Exercises
- Create a new Angular application with the Angular CLI and examine its structure.
- Modify the task management app to add functionality for deleting tasks.
- Create a new component that filters tasks (All, Active, Completed) and integrate it with the task list.
- Implement local storage in the task service so tasks persist between page refreshes.
- Create a nested component structure with a parent task list and child task item components.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)