Skip to content

Instantly share code, notes, and snippets.

@tshego3
Last active January 21, 2025 08:53
Show Gist options
  • Save tshego3/ba1a1b414c9a8b0e41c4db86ff2c365d to your computer and use it in GitHub Desktop.
Save tshego3/ba1a1b414c9a8b0e41c4db86ff2c365d to your computer and use it in GitHub Desktop.
Angular in 90ish Minutes - Developer Guide

This project was generated with Angular CLI version 18.2.7.

For the full code and examples, refer to the GitHub repository.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. What is Angular
  4. Angular Cookbook (Latest Edition)
  5. Benefits of Angular
  6. Angular vs React
  7. Myths about Angular
  8. Creating an Angular Application
  9. Angular Components
  10. Creating Angular Components
  11. Data-Binding in Angular
  12. Passing Data from Parent to Child Component
  13. Event Listeners in Angular
  14. Creating a Counter Component
  15. Creating Routes in Angular
  16. Angular Services
  17. Making HTTP Calls in Angular Services
  18. Angular Directives
  19. Angular Pipes
  20. Outro

Introduction

Angular development began in 2013 and has evolved into one of the most robust frameworks available today. This guide is inspired by the best practices and tools used by seasoned Angular experts, including content creators like John Papa and the Angular community. Whether you're a seasoned developer or a newcomer, this guide aims to provide a comprehensive overview of Angular.

Prerequisites

Before diving in, ensure you are familiar with the following:

What is Angular

Angular is a TypeScript-based open-source framework developed by Google, primarily for building dynamic, single-page web applications. It's characterized by:

  • Support for single-page applications (SPAs).
  • A large, active community and extensive documentation.
  • Usage in several Google applications (e.g., Google Analytics).

The latest Angular Cookbook provides over 90 practical projects. Key topics include:

  • Component communication
  • Directives and control flows
  • RxJS and NgRx for state management
  • Routing, forms, animations
  • Unit testing and performance optimization

Benefits of Angular

Angular provides the following benefits:

  • Faster development with less code.
  • Built-in unit testing.
  • Consistent coding standards due to its opinionated structure.
  • Enhanced reusability through components, services, and pipes.
  • Comprehensive CLI tools for faster code generation and deployment.

Angular vs React

Feature Angular React
Type Framework Library
CLI Built-in Requires additional tools
Built-in tools Many (e.g., forms, HTTP) Few (requires external libraries)
Opinionated Yes No

Myths about Angular

  • Hard to learn: Not true; modern Angular has extensive documentation and is user-friendly.
  • Frequent breaking changes: Angular's biannual updates are mostly incremental.
  • Slow performance: Recent benchmarks show Angular performs on par with Vue.js and React.

Creating an Angular Application

Use Angular CLI to create and manage projects efficiently:

Installing Angular CLI

  1. Ensure Node.js is installed. Verify with node -v and npm -v in your terminal.
  2. Install the Angular CLI globally:
    npm install -g @angular/[email protected]
    # OR
    npm install -g @angular/cli
    npm install -g @angular/[email protected]
  3. Verify the installation:
    ng --version
  4. Create a new Angular project:
    ng new first-ng-app
  5. Navigate to your project directory and serve the application:
    cd first-ng-app
    ng serve
    Visit http://localhost:4200 to see your app running.

This generates a project structure with essential files. Use ng serve to run the development server.

Angular Components

Components are the building blocks of Angular applications. A component consists of:

  • TypeScript: Logic and data binding.
  • HTML: The template.
  • SCSS: Styles scoped to the component.

Creating Angular Components

Run the Angular CLI command:

ng g c header # short form
ng generate component header # full form
# creates inside the `src/app` folder

# OR (in a nested directory)
ng g c components/header 
# creates HeaderComponent 
# inside the `src/app/components` folder

ng g c home
# creates the HomeComponent

This generates the component files:

  • header.component.ts (logic)
  • header.component.html (template)
  • header.component.scss (styles)

Example: Basic Component

//first-ng-app/src/app/components/header/header.component.html

HTML:

<h1>My first Angular app</h1>

first-ng-app/src/app/components/header/header.component.ts

TypeScript:

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

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss',
})
export class HeaderComponent {
}

For a complete implementation, refer to the header component code in the GitHub repository.

Data-Binding in Angular

Binding data between the TypeScript class of the component, and the component's template.

Data Binding with Modern Angular (with Signals):

  • A signal is a wrapper around a value that notifies interested consumers when that value changes. Signals can contain any value, from primitives to complex data structures.

//first-ng-app/src/app/components/header/header.component.html

HTML:

- <h1>My first Angular app</h1>
+ <h1>{{ title() }}</h1>

//first-ng-app/src/app/components/header/header.component.ts

TypeScript:

- + import { Component } from '@angular/core';
+ import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss',
})
export class HeaderComponent {
+ title = signal('My first Angular app');
}

Data Binding without Signals (traditional way):

//first-ng-app/src/app/components/header/header.component.html

HTML:

- <h1>My first Angular app</h1>
+ <h1>{{ title }}</h1>

//first-ng-app/src/app/components/header/header.component.ts

TypeScript:

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

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss',
})
export class HeaderComponent {
+ title = 'My first Angular app';
}

Angular supports:

  • One-way data binding: Display data in templates.
<p>{{ title }}</p>
  • Two-way data binding: Sync data between the model and view.
<input [(ngModel)]="title" />

Passing Data from Parent to Child Component

Passing data from parent to child component via Inputs. When you use a component, you commonly want to pass some data to it. A component specifies the data that it accepts by declaring inputs. We'll pass the greeting message from the AppComponent.

ng g c home
# generates in `src/app`

ng g c components/greeting
# generates in `src/app/components`

//first-ng-app/src/app/components/header/header.component.html

HTML:

+ <header>
+   <nav>
+       {{ title }}
+   </nav>
+ </header>

//first-ng-app/src/app/components/header/header.component.ts

TypeScript:

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

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss',
})
export class HeaderComponent {
+ title = 'My first Angular app';
}

//first-ng-app/src/app/components/greeting/greeting.component.html

HTML:

<h2>Greetings!</h2>
+ <p>{{ message() }}</p>

//first-ng-app/src/app/components/greeting/greeting.component.ts

TypeScript:

- + import { Component } from '@angular/core';
+ import { Component, input } from '@angular/core';

@Component({
  selector: 'app-greeting',
  standalone: true,
  imports: [],
  templateUrl: './greeting.component.html',
  styleUrl: './greeting.component.scss',
})
export class GreetingComponent {
+ message = input('Hello hello!');
}

//first-ng-app/src/app/home/home.component.html

HTML:

+ <app-greeting [message]="homeMessage()" />

//first-ng-app/src/app/home/home.component.ts

TypeScript:

- + import { Component } from '@angular/core';
+ import { Component, signal } from '@angular/core';
+ import { GreetingComponent } from '../components/greeting/greeting.component';

@Component({
  selector: 'app-home',
  standalone: true,
- imports: [],
+ imports: [GreetingComponent],
  templateUrl: './home.component.html',
  styleUrl: './home.component.scss',
})
export class HomeComponent {
+ homeMessage = signal('Hello, world!');
}

//first-ng-app/src/app/app.component.html

HTML:

+ <app-header />

//first-ng-app/src/app/app.component.ts

TypeScript:

import { Component } from '@angular/core';
+ import { HeaderComponent } from './components/header/header.component';

@Component({
  selector: 'app-root',
  standalone: true,
- imports: [],
+ imports: [HeaderComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
+ title = 'first-ng-app';
}

Event Listeners in Angular

//first-ng-app/src/app/home/home.component.html

HTML:

<app-greeting [message]="homeMessage()" />
+ <input
+   placeholder="Type something..."
+   type="text"
+   (keyup)="keyUpHandler($event)"
+ />

//first-ng-app/src/app/home/home.component.ts

TypeScript:

import { Component, signal } from '@angular/core';
import { GreetingComponent } from '../components/greeting/greeting.component';

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [GreetingComponent],
  templateUrl: './home.component.html',
  styleUrl: './home.component.scss',
})
export class HomeComponent {
  homeMessage = signal('Hello, world!');
+
+ keyUpHandler(event: KeyboardEvent) {
+   console.log(`user pressed the ${event.key} key`);
+ }
}

Creating a Counter Component

Build a counter with increment, decrement, and reset functionality:

ng g c components/counter

//first-ng-app/src/app/components/counter/counter.component.html

HTML:

+ <p>Counter value: {{ counterValue() }}</p>
+
+ <div>
+  <button (click)="increment()">Increment</button>
+  <button (click)="reset()">Reset</button>
+  <button (click)="decrement()">Decrement</button>
+ </div>

//first-ng-app/src/app/components/counter/counter.component.ts

TypeScript:

- import { Component } from '@angular/core';
+ import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  imports: [],
  templateUrl: './counter.component.html',
  styleUrl: './counter.component.scss',
})
export class CounterComponent {
+  counterValue = signal(0);
+  increment() {
+    this.counterValue.update((val) => val + 1);
+  }
+
+  decrement() {
+    this.counterValue.update((val) => val - 1);
+  }
+
+  reset() {
+    this.counterValue.set(0);
+  }
}

//first-ng-app/src/app/home/home.component.html

HTML:

<app-greeting [message]="homeMessage()" />
+ <app-counter />
<input
  placeholder="Type something..."
  type="text"
  (keyup)="keyUpHandler($event)"
/>

//first-ng-app/src/app/home/home.component.ts

TypeScript:

import { Component, signal } from '@angular/core';
import { GreetingComponent } from '../components/greeting/greeting.component';
+ import { CounterComponent } from '../components/counter/counter.component';

@Component({
  selector: 'app-home',
  standalone: true,
- imports: [GreetingComponent],
+ imports: [GreetingComponent, CounterComponent],
  templateUrl: './home.component.html',
  styleUrl: './home.component.scss',
})
export class HomeComponent {
  homeMessage = signal('Hello, world!');

  keyUpHandler(event: KeyboardEvent) {
    console.log(`user pressed the ${event.key} key`);
  }
}

Creating Routes in Angular

Angular is a single page application. Using routes, you can still define different pages that the user can navigate to. The browser only loads the bundles related to the route user has accessed. This significantly improves the performance of the app, and user experience.

Setting Up Routing

Let's create a new route. Create another component (as a page) for the route:

ng g c todos
# this will be the page for todos' list

Create a component for each todo item:

ng g c components/todo-item

//first-ng-app/src/app/app.routes.ts

TypeScript:

import { Routes } from '@angular/router';

- export const routes: Routes = [];
+ export const routes: Routes = [
+   {
+       path: '',
+       pathMatch: 'full',
+       loadComponent: () => {
+           return import('./home/home.component').then((m) => m.HomeComponent);
+       },
+   },
+   {
+       path: 'todos',
+       loadComponent: () => {
+           return import('./todos/todos.component').then((m) => m.TodosComponent);
+       },
+   },
+ ];

//first-ng-app/src/app/app.component.html

HTML:

<app-header />
+ <main>
+   <router-outlet />
+ </main>

//first-ng-app/src/app/app.component.ts

TypeScript:

import { Component } from '@angular/core';
+ import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';

@Component({
  selector: 'app-root',
  standalone: true,
- imports: [HeaderComponent],
+ imports: [RouterOutlet, HeaderComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  title = 'first-ng-app';
}

//first-ng-app/src/app/components/header/header.component.html

HTML:

<header>
  <nav>
-    {{ title }}
+    <span routerLink="/">{{ title }}</span>
+    <ul>
+      <li routerLink="/todos">Todos</li>
+    </ul>
  </nav>
</header>

//first-ng-app/src/app/components/header/header.component.ts

TypeScript:

import { Component } from '@angular/core';
+ import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-header',
  standalone: true,
- imports: [],
+ imports: [RouterLink],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss',
})
export class HeaderComponent {
  title = 'My first Angular app';
}

Visit http://localhost:4200 to see Home page.

Visit http://localhost:4200/todos to see Todos page.

Explore the complete routing setup in the repository’s source code.

Angular Services

Angular Services are used to encapsulate data, making HTTP calls, or performing any task that is not related directly to data rendering (in my opinion).

Creating an Angular Service:

ng g service services/todos
# creates todos.service.ts inside `src/app/services`

Example of serving data from an Angular Service:

Making HTTP Calls in Angular Services

  • Provide HTTP module/providers in the app config using provideHttpClient()
  • Inject the HttpClient service
  • Use the http methods

//first-ng-app/src/app/model/todo.type.ts

TypeScript:

+ export type Todo = {
+   userId: number;
+   completed: boolean;
+   title: string;
+   id: number;
+ };

//first-ng-app/src/app/services/todos.service.ts

TypeScript:

import { inject, Injectable } from '@angular/core';
+ import { Todo } from '../model/todo.type';
+ import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root',
})
export class TodosService {
-    
- constructor() {}
+ http = inject(HttpClient);
+ getTodosFromApi() {
+   const url = `https://jsonplaceholder.typicode.com/todos`;
+  return this.http.get<Array<Todo>>(url);
+ }
}

//first-ng-app/src/app/components/todo-item/todo-item.component.html

HTML:

+ <li [isCompleted]="todo().completed" >
+  <input
+    id="todo-{{ todo().id }}"
+    type="checkbox"
+    [checked]="todo().completed"
+    (change)="this.todoClicked()"
+  />
+  <label for="todo-{{ todo().id }}">{{ todo().title }}</label>
+ </li>

//first-ng-app/src/app/components/todo-item/todo-item.component.ts

TypeScript:

- import { Component } from '@angular/core';
+ import { Component, input, output } from '@angular/core';
+ import { Todo } from '../../model/todo.type';

@Component({
  selector: 'app-todo-item',
  standalone: true,
  imports: [],
  templateUrl: './todo-item.component.html',
  styleUrl: './todo-item.component.scss',
})
export class TodoItemComponent {
+ todo = input.required<Todo>();
+ todoToggled = output<Todo>();
+
+  todoClicked() {
+    this.todoToggled.emit(this.todo());
+  }
}

//first-ng-app/src/app/todos/todos.component.html

HTML:

+ <h3>Todos List</h3>
+
+ <ul>
+  <app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todoItems()[0]" />
+ </ul>

//first-ng-app/src/app/todos/todos.component.ts

TypeScript:

- import { Component } from '@angular/core';
+ import { Component, inject, OnInit, signal } from '@angular/core';
+ import { TodosService } from '../services/todos.service';
+ import { Todo } from '../model/todo.type';
+ import { TodoItemComponent } from '../components/todo-item/todo-item.component';

@Component({
  selector: 'app-todos',
  standalone: true,
- imports: [],
+ imports: [TodoItemComponent],
  templateUrl: './todos.component.html',
  styleUrl: './todos.component.scss',
})
export class TodosComponent implements OnInit {
+  todoService = inject(TodosService);
+  todoItems = signal<Array<Todo>>([]);
+
+  ngOnInit(): void {
+    this.todoService
+      .getTodosFromApi()
+      .subscribe((todos) => {
+        this.todoItems.set(todos);
+      });
+  }
+
+  updateTodoItem(todoItem: Todo) {
+    this.todoItems.update((todos) => {
+      return todos.map((todo) => {
+        if (todo.id === todoItem.id) {
+          return {
+            ...todo,
+            completed: !todo.completed,
+          };
+        }
+        return todo;
+      });
+    });
+  }
}

//first-ng-app/src/app/app.config.ts

TypeScript:

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
+   provideHttpClient(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
  ],
};

Angular Directives

Angular Directives allow you to add additional behavior to elements in our Angular applications.

Types of Angular Directives:

  • Components
  • Attribute directives [style.backgroundColor]
  • Structural directives *ngIf or @if, *ngFor or @for

Let's create an Angular directive for completed todos:

ng g directive directives/highlight-completed-todo

//first-ng-app/src/app/todos/todos.component.ts

HTML:

<h3>Todos List</h3>

+ @if (!todoItems().length) {
+   <p>Loading...</p>
+ }
+
<ul>
- <app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todoItems()[0]" />
+ @for (todo of todoItems(); track todo.id) {
+   <app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todo" />
+ }
</ul>

//first-ng-app/src/app/directives/highlight-completed-todo.directive.ts

TypeScript:

- import { Directive } from '@angular/core';
+ import { Directive, input, effect, inject, ElementRef } from '@angular/core';

@Directive({
  selector: '[appHighlightCompletedTodo]',
  standalone: true,
})
export class HighlightCompletedTodoDirective {
-    
- constructor() {}
+  isCompleted = input(false);
+  el = inject(ElementRef);
+  stylesEffect = effect(() => {
+    if (this.isCompleted()) {
+      this.el.nativeElement.style.textDecoration = 'line-through';
+      this.el.nativeElement.style.backgroundColor = '#d3f9d8';
+      this.el.nativeElement.style.color = '#6c757d';
+    } else {
+      this.el.nativeElement.style.textDecoration = 'none';
+      this.el.nativeElement.style.backgroundColor = '#fff';
+      this.el.nativeElement.style.color = '#000';
+    }
+  });
}

//first-ng-app/src/app/components/todo-item/todo-item.component.html

HTML:

- <li [isCompleted]="todo().completed" >
+ <li appHighlightCompletedTodo [isCompleted]="todo().completed" >
    <input
        id="todo-{{ todo().id }}"
        type="checkbox"
        [checked]="todo().completed"
        (change)="this.todoClicked()"
    />
    <label for="todo-{{ todo().id }}">{{ todo().title }}</label>
</li>

//first-ng-app/src/app/components/todo-item/todo-item.component.ts

TypeScript:

import { Component, input, output } from '@angular/core';
import { Todo } from '../../model/todo.type';
+ import { HighlightCompletedTodoDirective } from '../../directives/highlight-completed-todo.directive';

@Component({
  selector: 'app-todo-item',
  standalone: true,
- imports: [],
+ imports: [HighlightCompletedTodoDirective],
  templateUrl: './todo-item.component.html',
  styleUrl: './todo-item.component.scss',
})
export class TodoItemComponent {
  todo = input.required<Todo>();
  todoToggled = output<Todo>();

  todoClicked() {
    this.todoToggled.emit(this.todo());
  }
}

Angular Pipes

Angular pipes are used to transform data right in the templates.

Built-in Angular pipes

Let's create a todos filter pipe:

ng g pipe pipes/filter-todos

//first-ng-app/src/app/pipes/filter-todos.pipe.ts

TypeScript:

import { Pipe, PipeTransform } from '@angular/core';
+ import { Todo } from '../model/todo.type';

@Pipe({
  name: 'filterTodos',
  standalone: true,
})
export class FilterTodosPipe implements PipeTransform {
-  transform(value: , ...args: unknown[]) unknown {
+  transform(todos: Todo[], searchTerm: string): Todo[] {
+   if (!searchTerm) {
+     return todos;
+   }
+   const text = searchTerm.toLowerCase();
+   return todos.filter((todo) => {
+     return todo.title.toLowerCase().includes(text);
+   });
  }
}

//first-ng-app/src/app/components/todo-item/todo-item.component.html

HTML:

<li appHighlightCompletedTodo [isCompleted]="todo().completed" >
    <input
        id="todo-{{ todo().id }}"
        type="checkbox"
        [checked]="todo().completed"
        (change)="this.todoClicked()"
    />
-   <label for="todo-{{ todo().id }}">{{ todo().title }}</label>
+   <label for="todo-{{ todo().id }}">{{ todo().title | uppercase }}</label>
</li>

//first-ng-app/src/app/components/todo-item/todo-item.component.ts

TypeScript:

import { Component, input, output } from '@angular/core';
import { Todo } from '../../model/todo.type';
import { HighlightCompletedTodoDirective } from '../../directives/highlight-completed-todo.directive';
+ import { UpperCasePipe } from '@angular/common';

@Component({
  selector: 'app-todo-item',
  standalone: true,
- imports: [HighlightCompletedTodoDirective],
+ imports: [HighlightCompletedTodoDirective, UpperCasePipe],
  templateUrl: './todo-item.component.html',
  styleUrl: './todo-item.component.scss',
})
export class TodoItemComponent {
  todo = input.required<Todo>();
  todoToggled = output<Todo>();

  todoClicked() {
    this.todoToggled.emit(this.todo());
  }
}

//first-ng-app/src/app/todos/todos.component.html

HTML:

<h3>Todos List</h3>

@if (!todoItems().length) {
<p>Loading...</p>
}

+ <form>
+  <label>Filter Todos</label>
+  <input
+    name="searchTerm"
+    [(ngModel)]="searchTerm"
+    placeholder="Search todos..."
+  />
+ </form>
+
<ul class="todos">
- @for (todo of todoItems(); track todo.id) {
+ @for (todo of todoItems() | filterTodos : searchTerm(); track todo.id) {
    <app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todo" />
  }
</ul>

//first-ng-app/src/app/todos/todos.component.ts

TypeScript:

import { Component, inject, OnInit, signal } from '@angular/core';
import { TodosService } from '../services/todos.service';
import { Todo } from '../model/todo.type';
+ import { catchError } from 'rxjs';
import { TodoItemComponent } from '../components/todo-item/todo-item.component';
+ import { FormsModule } from '@angular/forms';
+ import { FilterTodosPipe } from '../pipes/filter-todos.pipe';

@Component({
  selector: 'app-todos',
  standalone: true,
- imports: [TodoItemComponent],
+ imports: [TodoItemComponent, FormsModule, FilterTodosPipe],
  templateUrl: './todos.component.html',
  styleUrl: './todos.component.scss',
})
export class TodosComponent implements OnInit {
  todoService = inject(TodosService);
  todoItems = signal<Array<Todo>>([]);
+ searchTerm = signal('');

  ngOnInit(): void {
    this.todoService
      .getTodosFromApi()
+     .pipe(
+       catchError((err) => {
+         console.log(err);
+         throw err;
+       })
+     )
      .subscribe((todos) => {
        this.todoItems.set(todos);
      });
  }

  updateTodoItem(todoItem: Todo) {
    this.todoItems.update((todos) => {
      return todos.map((todo) => {
        if (todo.id === todoItem.id) {
          return {
            ...todo,
            completed: !todo.completed,
          };
        }
        return todo;
      });
    });
  }
}

Outro

This guide captures the essence of Angular development. Explore more advanced topics and refine your skills through practical projects and community interaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment