Skip to content

The same app in Angular

Angular is a complete framework from Google with a router, forms, an HTTP client and a command line tool, as the chapter Frameworks compared describes. Components are classes with the decorator @Component and a template, and Angular uses TypeScript throughout. This page shows the catalog with search and favorites as two Angular components.

According to the project assignment, you may use Angular in your project too. It pays off if you need it at work or want to build a larger project with firm rules. You create a new project with ng new after npm install -g @angular/cli. Since version 22, a new major version comes out once a year and is usually supported for 24 months. Before you start, check which versions of Node and TypeScript it requires, Angular 22 needs TypeScript 6.0. The table in the section Transfer maps the terms of Vue and Angular onto each other.

The example under optional/angular/en does without the CLI. npm run build compiles the components with the Angular compiler ngc, which also checks the templates against the types. To see them in the browser, copy the files of src/app into a project of the CLI.

Signals hold the state, much like ref in Vue. signal('') creates a value that the template reads with query() and the code changes with set or update. computed derives the hits from it. @for needs a key through track, and @empty shows a text when the list is empty.

catalog.ts
import { Component, computed, signal } from '@angular/core'
import items from '../../data/items.json'
import type { Item } from './item'
import { ItemCard } from './item-card'
// Signals hold the state, computed derives the hits as computed does in Vue
@Component({
selector: 'app-catalog',
imports: [ItemCard],
template: `
<main>
<h1>Catalog</h1>
<label for="search">Search</label>
<input #search id="search" type="search" [value]="query()" (input)="query.set(search.value)" />
<p role="status">{{ hits().length }} things found, {{ favorites().length }} favorites</p>
<ul class="grid">
@for (item of hits(); track item.id) {
<li>
<app-item-card [item]="item" [favorite]="favorites().includes(item.id)" (toggleFavorite)="toggleFavorite($event)" />
</li>
} @empty {
<li>Nothing found.</li>
}
</ul>
</main>
`,
})
export class Catalog {
protected readonly query = signal('')
protected readonly favorites = signal<number[]>([])
protected readonly hits = computed(() => {
const text = this.query().trim().toLowerCase()
return (items as Item[]).filter((item) => item.name.toLowerCase().includes(text))
})
protected toggleFavorite(id: number) {
this.favorites.update((current) => (current.includes(id) ? current.filter((favorite) => favorite !== id) : [...current, id]))
}
}

The card gets its data through input.required() and input(), and output() reports the click, just as defineProps and defineEmits do in Vue. Since version 19 components are standalone, which is why the card is listed in the array imports of the catalog.

item-card.ts
import { Component, input, output } from '@angular/core'
import type { Item } from './item'
// Signal inputs take the place of props, an output the place of an emitted event
@Component({
selector: 'app-item-card',
template: `
<article class="card">
<h2>{{ item().name }}</h2>
<p>{{ item().category }}, CHF {{ item().deposit }} deposit</p>
<button type="button" [attr.aria-pressed]="favorite()" (click)="toggleFavorite.emit(item().id)">
{{ favorite() ? 'Remove favorite' : 'Save as favorite' }}
</button>
</article>
`,
})
export class ItemCard {
readonly item = input.required<Item>()
readonly favorite = input(false)
readonly toggleFavorite = output<number>()
}

The compiler finds mistakes in the templates. If the card is used without the input item, ngc reports the error NG8008, and if @for lacks track, the error NG5002. A signal without parentheses in the template is a type error, because only the call returns the value.

Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.