Credits to Maximillian Schwarzmüller. This lecture note are based on his Angular - The Complete Guide (2025 Edition).

1. Create Project

New Project with CSS, and disabled SSR
ng new vehicle-rental

1.1. index.html

  • beinhaltet nur die app-root - Komponente

  • Der Code in main.ts wird beim Laden der html-Seite ausgeführt.

Beim "Seitenquelltext anzeigen" sieht man die eingefügten Scripts.
  • Das File app.component.ts wird importiert und enthält die Funktionalität der Komponente.

vhcl 001 Decorator

1.2. Wireframe

vhcl 002 wireframe

2. Add a Component Manually

index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>VehicleRental</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-header></app-header>
  <app-root></app-root>
</body>
</html>
header.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-header',  (1)
  standalone: true,   (2)
  templateUrl: './header.component.html' (3)
})
export class HeaderComponent {
}
1 Der selector muss immer einen Bindestrich (Hyphen) beinhalten, damit es keine Überschneidungen mit derzeitigen oder zukünftigen HTML-Tags gibt.
2 ab Angular 19 ist der Default-Wert von standalone auf true gesetzt.
3 mit template kann direkt html-code verwendet werden, templateUrl verweist auf eine externe Datei.
header.component.html
<header>
  <h1>EasyRent</h1>
</header>
  • Beim Ausführen der Anwendung wird der Header nicht angezeigt, da die Komponente nicht in der main.ts-Datei registriert ist.

  • Man sieht in "Seitenquelltext anzeigen" den Header-Tag, er wird aber nicht gerendert.

  • In Angular wird die Anwendung durch das Bootstrapping gestartet.

  • Bootstrapping

main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { HeaderComponent } from './app/header.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));
bootstrapApplication(HeaderComponent);
  • JETZT SIEHT MAN DEN HEADER

SO WIRD ES ABER NICHT GEMACHT!
Wir wollen die Komponenten hierarchisch aufbauen, daher soll der header in der app-component eingebunden werden.
vhcl 014 component hierarchy
  • Die AppComponent ist die Wurzelkomponente und enthält alle anderen Komponenten.

  • Man bootstrapped daher nur die AppComponent in der main.ts-Datei.

  • One Angular Application = One Component Tree

main.ts - löschen der header component und ebenso in index.html löschen des header tags
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));
app.component.html
<app-header></app-header>
vhcl 014 error message not a known element
app.component.ts
import { Component } from '@angular/core';
import {HeaderComponent} from './header.component';

@Component({
  selector: 'app-root',
  // standalone: true,  // ab Angular 19 default auf true
  imports: [HeaderComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'vehicle-rental';
}
header.component.css
header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  width: 90%;
  max-width: 50rem;
  margin: 0 auto 2rem auto;
  text-align: center;
  background: linear-gradient(
    to bottom,
    #2c0a4c,
    #450d80
  );
  padding: 1rem;
  border-bottom-right-radius: 12px;
  border-bottom-left-radius: 12px;
  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
}

img {
  width: 3.5rem;
  object-fit: contain;
}

h1 {
  font-size: 1.25rem;
  margin: 0;
  padding: 0;
}

p {
  margin: 0;
  font-size: 0.8rem;
  text-wrap: balance;
}

@media (min-width: 768px) {
  header {
    padding: 2rem;
  }

  img {
    width: 4.5rem;
  }

  h1 {
    font-size: 1.5rem;
    margin: 0;
    padding: 0;
  }
}
  • We also have to update the global styles in the styles.css file.

/* You can add global styles to this file, and also import other style files */
* {
  box-sizing: border-box;
}

html {
  height: 100%;
}

body {
  font-family: "Poppins", sans-serif;
  background: radial-gradient(circle at top left, #181023, #0b0519);
  color: #c3b3d8;
  margin: 0;
  padding: 0;
  height: 100%;
}
  • Copy the file rent-a-car-logo-2.png into the public-folder

  • You could also use a src/assets folder, but for this example we use the public folder.

    Configuring the assets-folder
    ...
    "assets": [
      "src/assets",
      {
        "glob": "**/*",
        "input": "public"
      }
    ],
    ...
header.component.html
<header>
  <img src="rent-a-car-logo-3.png" alt="A vehicle rent list"/>
  <h1>EasyRent</h1>
  <p>Enterprise-level car rent management without friction</p>
</header>
index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>VehicleRental</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico" />
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link
    href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap"
    rel="stylesheet"
  />
</head>
<body>
  <app-root></app-root>
</body>
</html>
vhcl 004 public folder
Figure 1. add image to public folder
vehicle.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-header',
  standalone: true,
  templateUrl: './header.component.html',
  styleUrl: './header.component.css' (1)
})
export class HeaderComponent {
}
1 Das css-file muss noch referenziert werden.
vhcl 003 first page
  • Now create a header-Folder a refactor to move the header files into it.

    vhcl 005 move to header folder

3. Generate a component

ng g c vehicle
vhcl 006 create component vehicle
vehicle.component.css
div {
  border-radius: 6px;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

button {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  background-color: #433352;
  color: #c3b3d1;
  border: none;
  font: inherit;
  cursor: pointer;
  width: 100%;
  min-width: 10rem;
  text-align: left;
}

button:hover,
button:active,
.active {
  background-color: #9965dd;
  color: #150722;
}

img {
  width: 2rem;
  object-fit: contain;
  border-radius: 50%;
  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
}

span {
  margin: 0;
  padding: 0;
  font-size: 0.8rem;
  font-weight: normal;
}
vehicle.component.html
<div>
  <button>
    <img src="" alt="">
    <span>NAME</span>
  </button>
</div>
app.component.css
main {
  width: 90%;
  max-width: 50rem;
  margin: 2.5rem auto;
  display: grid;
  grid-auto-flow: row;
  gap: 2rem;
}

#vehicles {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 0.5rem;
  overflow: auto;
}

#fallback {
  font-weight: bold;
  font-size: 1.15rem;
  margin: 0;
  text-align: center;
}

@media (min-width: 768px) {
  main {
    margin: 4rem auto;
    grid-template-columns: 1fr 3fr;
  }

  #vehicles {
    flex-direction: column;
  }

  #fallback {
    font-size: 1.5rem;
    text-align: left;
  }
}
  • app.component.html

  • Strg+Space, Return

    vhcl 007 assist tag insertion
  • Return

    vhcl 008 import component automatically
  • Nun ist auch die Vehicle-Komponente in der app.component.ts eingebunden.

app.component.html
<app-header />
<app-vehicle />
app.component.html ändern, um einen schmäleren Button zu erhalten.
<app-header />

<main>
  <ul id="vehicles">
    <li>
      <app-vehicle/>
    </li>
  </ul>
</main>
vhcl 009 result

4. Output Dynamic Content

dummy-vehicles.ts
export const DUMMY_VEHICLES = [
  {
    id: 'v1',
    brand: 'VW Golf',
    avatar: 'compact.png'
  },
  {
    id: 'v2',
    brand: 'Honda Civic',
    avatar: 'coupe.png'
  },
  {
    id: 'v3',
    brand: 'Renault Espace',
    avatar: 'family.png'
  },
  {
    id: 'v4',
    brand: 'Opel Kapitän',
    avatar: 'limousine.png'
  },
  {
    id: 'v5',
    brand: 'Renault Clio',
    avatar: 'smallcar.png'
  },
  {
    id: 'v6',
    brand: 'Ford Mustang',
    avatar: 'sportscar.png'
  },
  {
    id: 'v7',
    brand: 'Dodge Warlock',
    avatar: 'truck.png'
  },
]
vehicle.component.ts
import { Component } from '@angular/core';
import { DUMMY_VEHICLES } from '../dummy-vehicles';

const randomIndex = Math.floor(Math.random() * DUMMY_VEHICLES.length);

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {
  selectedVehicle = DUMMY_VEHICLES[randomIndex];

}
Variablen mit Scope private sind nicht im html-File verfügbar.
vhcl 015 one way binding

4.1. String Interpolation

vehicle.component.html (String Interpolation)
<div>
  <button>
    <img src="icons/"/>
    <span>{{ selectedVehicle.brand }}</span>
  </button>
</div>

4.2. Property Binding

vehicle.component.html (Property Binding)
<div>
  <button>
    <img
      [src]="'icons/' + selectedVehicle.avatar"
      [alt]="selectedVehicle.brand"
    />
    <span>{{ selectedVehicle.brand }}</span>
  </button>
</div>

4.3. Using Getters for Computed Values

vehicle.component.html
<div>
  <button>
    <img
      [src]="imagePath"
      [alt]="selectedVehicle.brand"
    />
    <span>{{ selectedVehicle.brand }}</span>
  </button>
</div>
vehicle.component.ts
// ommitted for brevity

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {
  selectedVehicle = DUMMY_VEHICLES[randomIndex];

  get imagePath() {
    return 'icons/' + this.selectedVehicle.avatar;
  }
}

4.4. Listening to Events With Event Binding

vhcl 010 create method
vehicle.component.html
<div>
  <button (click)="onSelectVehicle()">
    <img
      [src]="imagePath"
      [alt]="selectedVehicle.brand"
    />
    <span>{{ selectedVehicle.brand }}</span>
  </button>
</div>
vehicle.component.ts
// ommitted for brevity

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {
  selectedVehicle = DUMMY_VEHICLES[randomIndex];

  get imagePath() {
    return 'icons/' + this.selectedVehicle.avatar;
  }

  onSelectVehicle() {
    console.log('Clicked!');
  }
}
vhcl 011 result

4.5. Managing State & Changing Data

  • Now we want to change the UI using a button click — i.e. the state of the selected vehicle should change.

vehicle.component.ts
// ommitted for brevity

const randomIndex = Math.floor(Math.random() * DUMMY_VEHICLES.length);

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {
  selectedVehicle = DUMMY_VEHICLES[randomIndex];

  get imagePath() {
    return 'icons/' + this.selectedVehicle.avatar;
  }

  onSelectVehicle() {
    const randomIndex = Math.floor(Math.random() * DUMMY_VEHICLES.length);
    this.selectedVehicle = DUMMY_VEHICLES[randomIndex];
  }
}

4.6. Angular’s Change Detection Mechanism

vhcl 012 signals vs zonejs

4.7. Introducing Signals

vhcl 013 signals
vehicle.component.html
1
2
3
4
5
6
7
8
9
<div>
  <button (click)="onSelectVehicle()">
    <img
      [src]="imagePath()"  (1)
      [alt]="selectedVehicle().brand"   (1)
    />
    <span>{{ selectedVehicle().brand }}</span> (1)
  </button>
</div>
1 Signals werden wie eine Methode mit runden Klammern angesprochen

4.8. Defining Components Inputs

  • Now we will notice that component are not only used to divide the application into smaller parts, but also to use components multiple times.

vehicle.component.ts - We remove the lines, which are not needed anymore (DUMMY_VEHICLES, randomIndex, selectedVehicle, getter).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { Component } from '@angular/core';


@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

  onSelectVehicle() {

  }
}
app.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { Component } from '@angular/core';
import { HeaderComponent } from './header/header.component';
import { VehicleComponent } from './vehicle/vehicle.component';
import { DUMMY_VEHICLES } from './dummy-vehicles';

@Component({
  selector: 'app-root',
  imports: [HeaderComponent, VehicleComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  vehicles = DUMMY_VEHICLES
}
  • vehicle.component.ts

vhcl 015 missing initializer
  • In TypeScript, the exclamation mark (!) used in the context of @Input() avatar!: string; is called the definite assignment assertion. It tells the TypeScript compiler that you, as the developer, are certain that the avatar property will be assigned a value before it’s used, even though the compiler can’t statically prove it.

  • Declaring inputs with the @Input decorator

    vhcl 017 input decorator deprecated
    We are using the @Input decorator to define the input properties of the component. Even this is deprecated, there are tons of projects using it, so it will not be removed in the near future.
vehicle.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

  @Input() avatar!: string;
  @Input() brand!: string;

  get imagePath() {
    return 'icons/' + this.avatar;
  }

  onSelectVehicle() {}
}
vehicle.component.html
1
2
3
4
5
6
7
8
9
<div>
  <button (click)="onSelectVehicle()">
    <img
      [src]="imagePath"
      [alt]="brand"
    />
    <span>{{ brand }}</span>
  </button>
</div>
app.component.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<app-header/>

<main>
  <ul id="vehicles">
    <li>
      <app-vehicle [avatar]="vehicles[0].avatar" [brand]="vehicles[0].brand" />
    </li>
    <li>
      <app-vehicle [avatar]="vehicles[1].avatar" [brand]="vehicles[1].brand" />
    </li>
    <li>
      <app-vehicle [avatar]="vehicles[2].avatar" [brand]="vehicles[2].brand" />
    </li>
    <li>
      <app-vehicle [avatar]="vehicles[3].avatar" [brand]="vehicles[3].brand" />
    </li>
  </ul>
</main>
vhcl 016 result

4.9. Required and Optional Inputs

vhcl 018 error required input

4.10. Using Signal Inputs

  • input

  • input<T> is a generic type.

Variante 1
avatar = input(''); // mit Default-Wert
Variante 2
avatar = input<string>(); // with generic type
Variante 3
avatar = input.required<string>(); // with required value
vehicle.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, Input, input, computed } from '@angular/core';

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

  // @Input({ required: true }) avatar!: string;
  // @Input({ required: true }) brand!: string;
  avatar = input.required<string>();  (1)
  brand = input.required<string>();   (1)

  // get imagePath() {
  //   return 'icons/' + this.avatar;
  // }
  imagePath = computed(() => 'icons/' + this.avatar() );



  onSelectVehicle() {
  }
}
1 These inputs are read-only. You can’t change them from within the component.
app.component.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<app-header/>

<main>
  <ul id="vehicles">
    <li>
      <app-vehicle [avatar]="vehicles[0].avatar" [brand]="vehicles[0].brand" />  (1)
    </li>
    <li>
      <app-vehicle [avatar]="vehicles[1].avatar" [brand]="vehicles[1].brand" /> (2)
    </li>
    <li>
      <app-vehicle [avatar]="vehicles[2].avatar" [brand]="vehicles[2].brand" /> (3)
    </li>
    <li>
      <app-vehicle [avatar]="vehicles[3].avatar" [brand]="vehicles[3].brand" /> (4)
    </li>
  </ul>
</main>
1 From outside, the component is used as before, when no signal was used. You can still use property binding to pass data to the component. The value itself (vehicles[0].avatar) has not to be a signal.

But you also have to consider, that the value itself is no signal. It is the DUMMY_VEHICLES-array, which is a plain array and no signal.

vehicle.component.html
1
2
3
4
5
6
7
8
9
<div>
  <button (click)="onSelectVehicle()">
    <img
      [src]="imagePath()"
      [alt]="brand()"
    />
    <span>{{ brand() }}</span>
  </button>
</div>
  • But in the component itself, you have to deal with signals to access the values.

4.11. Custom Events with @Output

  • Initial State

vehicle.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, computed, Input, input } from '@angular/core';

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

  @Input({ required: true }) avatar!: string;
  @Input({ required: true }) brand!: string;
  // avatar = input.required<string>();
  // brand = input.required<string>();

  get imagePath() {
    return 'icons/' + this.avatar;
  }
  // imagePath = computed(() => 'icons/' + this.avatar() );

  onSelectVehicle() {
  }
}
vehicle.component.html
1
2
3
4
5
6
7
8
9
<div>
  <button (click)="onSelectVehicle()">
    <img
      [src]="imagePath"
      [alt]="brand"
    />
    <span>{{ brand }}</span>
  </button>
</div>
  • Now we emit an event from the VehicleComponent to the AppComponent when the button is clicked.

vehicle.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

  @Input({required:true}) id!: string
  @Input({ required: true }) avatar!: string
  @Input({ required: true }) brand!: string
  @Output() select = new EventEmitter() (1)

  get imagePath() {
    return 'icons/' + this.avatar;
  }

  onSelectVehicle() {
    this.select.emit(this.id)
  }
}
1 The Event Emitter should be typed with the type of the emitted value, ie @Output() select = new EventEmitter<string>()
app.component.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<app-header/>

<main>
  <ul id="vehicles">
    <li>
      <app-vehicle [id]="vehicles[0].id"
                   [avatar]="vehicles[0].avatar"
                   [brand]="vehicles[0].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
    <li>
      <app-vehicle [id]="vehicles[1].id"
                   [avatar]="vehicles[1].avatar"
                   [brand]="vehicles[1].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
    <li>
      <app-vehicle [id]="vehicles[2].id"
                   [avatar]="vehicles[2].avatar"
                   [brand]="vehicles[2].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
    <li>
      <app-vehicle [id]="vehicles[3].id"
                   [avatar]="vehicles[3].avatar"
                   [brand]="vehicles[3].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
  </ul>

 RENTALS OF THAT VEHICLE
</main>
  • the $event object will contain the id, which was passed bei onSelectVehicle()-method.

app.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';
import { HeaderComponent } from './header/header.component';
import { VehicleComponent } from './vehicle/vehicle.component';
import { DUMMY_VEHICLES } from './dummy-vehicles';

@Component({
  selector: 'app-root',
  imports: [HeaderComponent, VehicleComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  vehicles = DUMMY_VEHICLES

  onSelectVehicle(id: string) {
    console.log('Selected vehicle with ' + id);
  }
}
vhcl 019 result

4.12. Custom events with output<…​>()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, EventEmitter, Input, output, Output } from '@angular/core';

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

  @Input({required:true}) id!: string
  @Input({ required: true }) avatar!: string
  @Input({ required: true }) brand!: string
  // @Output() select = new EventEmitter()
  select = output<string>();
   (1) (2)

  get imagePath() {
    return 'icons/' + this.avatar;
  }

  onSelectVehicle() {
    this.select.emit(this.id)
  }
}
1 The output()-function is a shorthand for creating an EventEmitter.
2 The generic type of the output()-function is the type of the emitted value (Rückgabewert).

4.13. Create a Configurable Component

  • Übung

    • Erstellen Sie eine neue Komponente RentalsComponent, die den Markennamen des ausgewählten Fahrzeugs anzeigt.

    • Binden Sie die neue Komponente in die AppComponent ein.

    • Übergeben Sie den Markennamen des ausgewählten Fahrzeugs an die RentalsComponent.

  • Lab

    • Create a new component RentalsComponent that displays the brand name of the selected vehicle.

    • Integrate the new component into the AppComponent.

    • Pass the brand name of the selected vehicle to the RentalsComponent.

vhcl 021 lab result
  • Lösung / Solution

ng g c rentals --skip-tests
rentals.component.html
<h2>{{ brand }}</h2>
rentals.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-rentals',
  imports: [],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {

  @Input({ required: true }) brand!: string

}
app.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Component } from '@angular/core';
import { HeaderComponent } from './header/header.component';
import { VehicleComponent } from './vehicle/vehicle.component';
import { DUMMY_VEHICLES } from './dummy-vehicles';
import { RentalsComponent } from './rentals/rentals.component';

@Component({
  selector: 'app-root',
  imports: [
    HeaderComponent,
    VehicleComponent,
    RentalsComponent
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  vehicles = DUMMY_VEHICLES
  selectedVehicleId = 'v1'

  get selectedVehicle() {
    return this.vehicles.find(
      (vehicle) => vehicle.id === this.selectedVehicleId
    )!  (1)
  }

  onSelectVehicle(id: string) {
    this.selectedVehicleId = id
  }

}
1 The ! operator is used to tell TypeScript that the value will never be null or undefined.
app.component.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<app-header/>

<main>
  <ul id="vehicles">
    <li>
      <app-vehicle [id]="vehicles[0].id"
                   [avatar]="vehicles[0].avatar"
                   [brand]="vehicles[0].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
    <li>
      <app-vehicle [id]="vehicles[1].id"
                   [avatar]="vehicles[1].avatar"
                   [brand]="vehicles[1].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
    <li>
      <app-vehicle [id]="vehicles[2].id"
                   [avatar]="vehicles[2].avatar"
                   [brand]="vehicles[2].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
    <li>
      <app-vehicle [id]="vehicles[3].id"
                   [avatar]="vehicles[3].avatar"
                   [brand]="vehicles[3].brand"
                   (select)="onSelectVehicle($event)"
      />
    </li>
  </ul>

  <app-rentals [brand]="selectedVehicle.brand" />

</main>
Don’t forget to import RentalsComponent in the app.component.ts file.

4.14. Potentially Undefined Values and Union Types

  • Without the ! operator, TypeScript would throw an error because the find() method could return undefined.

  • The ! operator is a way to tell TypeScript that the value will never be null or undefined. But it is not a good practice to use it in this way, because it is only a promise to TypeScript, but not a guarantee.

  • A ?-operator tells TypeScript that the value could be null or undefined.

  • The selectedVehicle property could be null or undefined if the find() method does not find a vehicle with the given id.

vhcl 022 possibly undefined
vhcl 023 error possibly undefined
  • The usage of ? is not helpful in this case.

vhcl 024 question mark
  • Alternativen:

rentals.component.ts
@Input() brand: string | undefined
  • The undefined type (the pipe symbol) is a union type, which means that the value could be a string or undefined.

rentals.component.ts - but the question mark is shorter
@Input() brand?: string
vhcl 025 now it works
Figure 2. string or undefined

4.15. Accepting Objects as Inputs

before
// ...
export class VehicleComponent {

  @Input({required:true}) id!: string
  @Input({ required: true }) avatar!: string
  @Input({ required: true }) brand!: string

  select = output<string>();

  get imagePath() {
    return 'icons/' + this.avatar;
  }

  onSelectVehicle() {
    this.select.emit(this.id)
  }

  // ...
}
afterward, with Object-Type
// ...
export class VehicleComponent {

  @Input({required: true}) vehicle!: {  (1)
    id: string
    avatar: string
    name: string
  }

  // @Output() select = new EventEmitter()
  select = output<string>();

  get imagePath() {
    return 'icons/' + this.vehicle.avatar; (2)
  }

  onSelectVehicle() {
    this.select.emit(this.vehicle.id) (3)
  }
}
1 define the object-type
2 access the object properties
3 access the object properties once more
vehicle.component.html
<div>
  <button (click)="onSelectVehicle()">
    <img
      [src]="imagePath"
      [alt]="vehicle.brand"  (1)
    />
    <span>{{ vehicle.brand }}</span> (2)
  </button>
</div>
app.component.html
<app-header/>

<main>
  <ul id="vehicles">
    <li>
      <app-vehicle [vehicle]="vehicles[0]" (select)="onSelectVehicle($event)" />
    </li>
    <li>
      <app-vehicle [vehicle]="vehicles[1]" (select)="onSelectVehicle($event)" />
    </li>
    <li>
      <app-vehicle [vehicle]="vehicles[2]" (select)="onSelectVehicle($event)" />
    </li>
    <li>
      <app-vehicle [vehicle]="vehicles[3]" (select)="onSelectVehicle($event)" />
    </li>
  </ul>


  <app-rentals [brand]="selectedVehicle ? selectedVehicle.brand : ''"/>

</main>

4.16. TypeScript: Type Aliases and Interfaces

4.16.1. Object Type

// ...

 type Vehicle = {
   id: string
   brand: string
   avatar: string
 }

export class VehicleComponent {

  @Input({required: true}) vehicle!: Vehicle

  select = output<string>();

  get imagePath() {
    return 'icons/' + this.vehicle.avatar; (1)
  }

  onSelectVehicle() {
    this.select.emit(this.vehicle.id) (2)
  }
}
vhcl 026 object type
Figure 3. object type
  • An alternative to the object type is the interface

  • In angular the interface is used more often than the object type (but it is a matter of taste)

  • According to ChatGPT: In Angular (and TypeScript in general), developers often prefer interfaces over object types (classes) for several reasons:

    1. Type Safety Without Runtime Overhead

      1. Interfaces are purely a TypeScript construct and do not exist in JavaScript at runtime. This means they do not add extra code to the final JavaScript output, making the application more lightweight.

      2. Classes, on the other hand, remain in the compiled JavaScript, potentially increasing the bundle size.

    2. Structural Typing (Duck Typing)

      1. TypeScript uses structural typing, meaning two objects are considered the same type if they have the same shape. This makes interfaces more flexible compared to classes.

        interface User {
          id: number;
          name: string;
        }
        
        function printUser(user: User) {
          console.log(user.name);
        }
        
        const person = { id: 1, name: "Alice", age: 25 };
        printUser(person); // ✅ Works, extra properties are ignored
    3. Better Readability and Simplicity

      1. Interfaces are simple and easy to understand, especially for defining data structures (DTOs, API responses, form models, etc.).

4.16.2. Interface

// ...

 interface Vehicle {
   id: string
   brand: string
   avatar: string
 }

export class VehicleComponent {

  @Input({required: true}) vehicle!: Vehicle

  select = output<string>();

  get imagePath() {
    return 'icons/' + this.vehicle.avatar; (1)
  }

  onSelectVehicle() {
    this.select.emit(this.vehicle.id) (2)
  }
}

4.17. Dynamic Output of List Content

app.component.html
<app-header/>

<main>
  <ul id="vehicles">
    @for (vehicle of vehicles; track vehicle.id) { (1)

      <li>
        <app-vehicle [vehicle]="vehicle"
                     (select)="onSelectVehicle($event)"/>
      </li>
    }
  </ul>

  <app-rentals [brand]="selectedVehicle ? selectedVehicle.brand : ''"/>

</main>

<.> <.> when the list changes, track is used to keep the state of the list items. So the DOM is not completely re-rendered.

vhcl 027 result

4.18. Outputting Conditional Content

  • The RentalsComponent should only be displayed when a vehicle is selected.

export class AppComponent {
  vehicles = DUMMY_VEHICLES
  selectedVehicleId?: string  (1)

  // ...
}
1 We remove the initial value of the selectedVehicleId property to make it undefined.
  • Now the app-rentals component is still rendered, but the content is empty.

  • We can use the @if directive to conditionally render the RentalsComponent.

<app-header/>

<main>
  <ul id="vehicles">
    // ...
  </ul>

  @if (selectedVehicle) {
    <app-rentals [brand]="selectedVehicle.brand"/>  (1)
  } @else {  (2)
    <p id="fallback">Select a vehicle to see its rentals!</p>
  }

</main>
1 Now we can remove the ternary operator because the selectedVehicle property is always defined when the RentalsComponent is rendered.
2 The @else directive is used to render a fallback message when no vehicle is selected.
  • @for, @if, @else, …​ are introduced in Angular 17.

4.19. Legacy Angular Directives ngFor anf ngIf

  • These are structural directives.

app.component.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<app-header/>

<main>
  <ul id="vehicles">
    <!--    @for (vehicle of vehicles; track vehicle.id) {-->

    <li *ngFor="let vehicle of vehicles">
      <app-vehicle [vehicle]="vehicle"
                   (select)="onSelectVehicle($event)"/>
    </li>
    <!--    }-->
  </ul>

  <!--  @if (selectedVehicle) {-->
  <!--    <app-rentals [brand]="selectedVehicle ? selectedVehicle.brand : ''"/>-->
  <app-rentals *ngIf="selectedVehicle; else fallback"
               [brand]="selectedVehicle!.brand"/>
  <!--  } @else {-->
  <ng-template #fallback>
    <p id="fallback">Select a vehicle to see its rentals!</p>
  </ng-template>

  <!--  }-->

</main>
  • and ngFor' and `ngIf has to be unlocked in the app.component.ts file.

app.component.ts
import { Component } from '@angular/core';
import { HeaderComponent } from './header/header.component';
import { VehicleComponent } from './vehicle/vehicle.component';
import { DUMMY_VEHICLES } from './dummy-vehicles';
import { RentalsComponent } from './rentals/rentals.component';
import { NgFor, NgIf } from '@angular/common';

@Component({
  selector: 'app-root',
  imports: [
    HeaderComponent,
    VehicleComponent,
    RentalsComponent,
    NgFor,
    NgIf
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  vehicles = DUMMY_VEHICLES
  selectedVehicleId?: string

  get selectedVehicle() {
    return this.vehicles.find(
      (vehicle) => vehicle.id === this.selectedVehicleId
    )
  }

  onSelectVehicle(id: string) {
    this.selectedVehicleId = id
  }

}
  • But we are using modern angular-versions, so we will use the new directives.

4.20. More Components

rentals.component.html
<section id="rentals">
  <header>
    <h2>{{ brand }}'s Rentals</h2>
    <menu>
      <button>Add Rental</button>
    </menu>
  </header>

  <ul>
    <li>

    </li>
  </ul>
</section>
  • With this css the rentals-section is displayed in a nice way.

rentals.component.css
#rentals {
  padding: 1rem;
  border-radius: 8px;
  max-height: 60vh;
  overflow: auto;
  background-color: #3a2c54;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 2rem;
  gap: 1rem;
}

h2 {
  margin: 0;
  font-size: 0.9rem;
  width: 60%;
  text-wrap: balance;
}

menu {
  margin: 0;
  padding: 0;
}

menu button {
  font: inherit;
  cursor: pointer;
  background-color: #9965dd;
  border-radius: 4px;
  border: none;
  padding: 0.35rem 0.8rem;
  font-size: 0.9rem;
}

menu button:hover,
menu button:active {
  background-color: #a565dd
}

ul {
  list-style: none;
  margin: 1rem 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-height: 50vh;
  overflow: auto;
}

@media (min-width: 768px) {
  h2 {
    font-size: 1.25rem;
  }

  menu {
    width: auto;
  }
}
  • For the rentals we need a new component.

ng g c rentals/rental --skip-tests
vhcl 028 new rental component
vhcl 029 import app rental
rentals.component.ts - Import of app-rental is added
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';

@Component({
  selector: 'app-rentals',
  imports: [
    RentalComponent
  ],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {

  @Input({ required: true }) brand?: string

}
rentals.component.html
<section id="rentals">
  <header>
    <h2>{{ brand }}'s Rentals</h2>
    <menu>
      <button>Add Rental</button>
    </menu>
  </header>

  <ul>
    <li>
      <app-rental />
    </li>
    <li>
      <app-rental />
    </li>
    <li>
      <app-rental />
    </li>
  </ul>
</section>
  • Add the stye-sheet to the rental component.

rental.component.css
article {
  padding: 1rem;
  color: #25113d;
  background-color: #bf9ee5;
}

h2 {
  margin: 0;
}

time {
  color: #3c2c50;
}

.actions {
  text-align: right;
  margin: 0;
}

.actions button {
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  background-color: #380774;
  color: #decdf2;
  border-radius: 4px;
  padding: 0.5rem 1.5rem;
  border: none;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: all 0.3s ease;
}

.actions button:hover,
.actions button:active {
  background-color: #4a0774;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.3);
}
vhcl 030 result

4.21. Outputting Vehicle-specific Rentals

  • Now we create some rentals for the vehicles.

rentals.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';

@Component({
  selector: 'app-rentals',
  imports: [
    RentalComponent
  ],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {
  @Input() brand?: string

  rentals = [
    {
      id: 'r1',
      vehicleId: 'v1',
      title: 'Susi rented a car',
      summary: 'Susi rented a car for going to the beach',
      dueDate: '2025-04-01'
    },
    {
      id: 'r2',
      vehicleId: 'v3',
      title: 'Hansi rented a car',
      summary: 'Hansi rented a car for going to the mountains',
      dueDate: '2025-04-03'
    },
    {
      id: 'r3',
      vehicleId: 'v3',
      title: 'Berta rented a car',
      summary: 'Berta rented also a car for going to the mountains',
      dueDate: '2025-04-07'
    },
  ];
}
rentals.component.html
<section id="rentals">
  <header>
    <h2>{{ brand }}'s Rentals</h2>
    <menu>
      <button>Add Rental</button>
    </menu>
  </header>

  <ul>
    @for (rental of rentals; track rental.id) {
      <li>
        <app-rental/>
      </li>
    }
  </ul>
</section>
  • This doesn’t change the output, because the rentals are still hard-coded.

vhcl 030 result
  • Now we add a filter to the rentals.

rentals.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';

@Component({
  selector: 'app-rentals',
  imports: [
    RentalComponent
  ],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {
  @Input({ required: true }) vehicleId!: string
  @Input({ required: true }) brand!: string

  rentals = [
    {
      id: 'r1',
      vehicleId: 'v1',
      title: 'Susi rented a car',
      summary: 'Susi rented a car for going to the beach',
      dueDate: '2025-04-01'
    },
    {
      id: 'r2',
      vehicleId: 'v3',
      title: 'Hansi rented a car',
      summary: 'Hansi rented a car for going to the mountains',
      dueDate: '2025-04-03'
    },
    {
      id: 'r3',
      vehicleId: 'v3',
      title: 'Berta rented a car',
      summary: 'Berta rented also a car for going to the mountains',
      dueDate: '2025-04-07'
    },
  ];

  get selectedVehicleRentals() {
    return this.rentals.filter((rental) => rental.vehicleId === this.vehicleId);
  }
}
app.component.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<app-header/>

<main>
  <ul id="vehicles">
    @for (vehicle of vehicles; track vehicle.id) {

      <li>
        <app-vehicle [vehicle]="vehicle"
                     (select)="onSelectVehicle($event)"/>
      </li>
    }
  </ul>

  @if (selectedVehicle) {
    <app-rentals [vehicleId]="selectedVehicle.id" [brand]="selectedVehicle.brand"/>
  } @else {
    <p id="fallback">Select a vehicle to see its rentals!</p>
  }

</main>
vhcl 031 result

4.22. Outputting Rental Data in the Rental Component

rental.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, Input } from '@angular/core';

interface Rental {
  id: string
  vehicleId: string
  title: string
  summary: string
  dueDate: string
}


@Component({
  selector: 'app-rental',
  imports: [],
  templateUrl: './rental.component.html',
  styleUrl: './rental.component.css'
})
export class RentalComponent {

  @Input({ required: true }) rental!: Rental

}
  • First we have to declare the rental interface.

  • Then we can use the rental interface in the RentalComponent to define the input property.

rentals.component.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<section id="rentals">
  <header>
    <h2>{{ brand }}'s Rentals</h2>
    <menu>
      <button>Add Rental</button>
    </menu>
  </header>

  <ul>
    @for (rental of selectedVehicleRentals; track rental.id) {
      <li>
        <app-rental [rental]="rental" /> (1)
      </li>
    }
  </ul>
</section>
1 Now we bind the rental object to the rental component.
rental.component.html
<article>
  <h2>{{rental.title}}</h2>
  <time>{{rental.dueDate}}</time>
  <p>{{rental.summary}}</p>
  <p class="actions">
    <button>Complete</button>
  </p>
</article>
vhcl 032 result

4.23. Storing Data Models in Separate Files

  • create a new file: vehicle.model.ts in the vehicle-folder

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

// remove the interface from the component

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

  // ommitted for brevity

}
vehicle.model.ts
export interface Vehicle {
  id: string
  brand: string
  avatar: string
}
  • you have to export the interface, so it can be used in other files.

  • Now we can import the interface in the vehicle.component.ts file.

vehicle.component.ts
import { Component, EventEmitter, Input, output, Output } from '@angular/core';
import type { Vehicle } from './vehicle.model';

// remove the interface from the component

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {

    @Input({ required: true }) vehicle!: Vehicle;
    // ommitted for brevity

}
  • Now we do the same for the rental component.

rental.component.ts
import { Component, Input } from '@angular/core';
import type { Rental } from './rental.model';  (1)

// remove the rental-interface from the component

@Component({
  selector: 'app-rental',
  imports: [],
  templateUrl: './rental.component.html',
  styleUrl: './rental.component.css'
})
export class RentalComponent {

  @Input({ required: true }) rental!: Rental

}
  • After inserting the interface in rental.model.ts, this import will be inserted automatically.

  • you can add the type-keyword to make clear it is a type-import.

    • import type only imports declarations to be used for type annotations and declarations. It always gets fully erased, so there’s no remnant of it at runtime. So, sometimes you can avoid odd errors when using import type.

    • Type-Only Imports and Export

rental.model.ts
export interface Rental {
  id: string
  vehicleId: string
  title: string
  summary: string
  dueDate: string
}

4.24. Dynamic CSS Styling with Class Bindings

  • Now we want to highlight the selected vehicle in the list of vehicles-buttons.

  • In the vehicle.component.css is already a class called .active, which we can use to highlight the selected vehicle.

vehicle.component.css
...
button:hover,
button:active,
.active {
  background-color: #9965dd;
  color: #150722;
}
...
  • We know already which vehicle is selected, because we have the selectedVehicleId property in the AppComponent.

app.component.html
<app-header/>

<main>
  <ul id="vehicles">
    @for (vehicle of vehicles; track vehicle.id) {

      <li>
        <app-vehicle [selected]="vehicle.id === selectedVehicleId"
                     [vehicle]="vehicle"
                     (select)="onSelectVehicle($event)"/>
      </li>
    }
  </ul>

  @if (selectedVehicle) {
    <app-rentals [vehicleId]="selectedVehicle.id"
                 [brand]="selectedVehicle.brand"/>
  } @else {
    <p id="fallback">Select a vehicle to see its rentals!</p>
  }

</main>
import { Component, EventEmitter, Input, output, Output } from '@angular/core';
import type { Vehicle } from './vehicle.model';

@Component({
  selector: 'app-vehicle',
  imports: [],
  templateUrl: './vehicle.component.html',
  styleUrl: './vehicle.component.css'
})
export class VehicleComponent {
  @Input({ required: true }) vehicle!: Vehicle;
  @Input({ required: true }) selected!: boolean;
  // @Output() select = new EventEmitter()
  select = output<string>();

  get imagePath() {
    return 'icons/' + this.vehicle.avatar;
  }

  onSelectVehicle() {
    this.select.emit(this.vehicle.id)
  }
}
vehicle.component.html
<div>
  <button [class.active]="selected"
          (click)="onSelectVehicle()">
    <img
      [src]="imagePath"
      [alt]="vehicle.brand"
    />
    <span>{{ vehicle.brand }}</span>
  </button>
</div>
vhcl 033 result

4.25. More Component Communication: Deleting Rentals

  • Now we want to delete a rental from the list of rentals.

  • For a vehicle we can have multiple rentals. The vehicles are booked by different users. When the user returns the vehicle, the rental is deleted.

    1. Add a click-handler to the rental component.

rental.component.html
<article>
  <h2>{{rental.title}}</h2>
  <time>{{rental.dueDate}}</time>
  <p>{{rental.summary}}</p>
  <p class="actions">
    <button (click)="onCompleteRental()">Complete</button>
  </p>
</article>
  1. Add an Output property to the rental component and a method to emit an event when the button is clicked.

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

@Component({
  selector: 'app-rental',
  imports: [],
  templateUrl: './rental.component.html',
  styleUrl: './rental.component.css'
})
export class RentalComponent {

  @Input({ required: true }) rental!: Rental
  @Output() complete = new EventEmitter<string>()

  onCompleteRental() {
    this.complete.emit(this.rental.id)
  }

}
  1. Now we catch the event and call the method onCompleteRental($event).

rentals.component.html
<section id="rentals">
  <header>
    <h2>{{ brand }}'s Rentals</h2>
    <menu>
      <button>Add Rental</button>
    </menu>
  </header>

  <ul>
    @for (rental of selectedVehicleRentals; track rental.id) {
      <li>
        <app-rental [rental]="rental" (complete)="onCompleteRental($event)"/>
      </li>
    }
  </ul>
</section>
  1. Finally, we implement the method onCompleteRental() in the rentals component.

rentals.component.ts
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';

@Component({
  selector: 'app-rentals',
  imports: [RentalComponent],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {
  @Input({ required: true }) vehicleId!: string
  @Input({ required: true }) brand!: string

  rentals = [
    // ommitted for brevity
  ];

  get selectedVehicleRentals() {
    return this.rentals.filter((rental) => rental.vehicleId === this.vehicleId);
  }

  onCompleteRental(id: string) {
    // ...
  }
}
  1. The onCompleteRental() method should remove the rental from the list of rentals. For this we can use the filter() method.

rentals.component.ts
onCompleteRental(id: string) {
  this.rentals = this.rentals.filter((rental) => rental.id !== id);
}

4.26. Creating and Conditionally Rendering Another Component

Create a new component.
ng g c new-rental --skip-tests
vhcl 034 new component
add a boolean property isAddingRental to the rentals component and a method to toggle the property.
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';
import { NewRentalComponent } from './new-rental/new-rental.component';

@Component({
  selector: 'app-rentals',
  imports: [RentalComponent, NewRentalComponent],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {
  @Input({ required: true }) vehicleId!: string
  @Input({ required: true }) brand!: string
  isAddingRental = false;

  rentals = [
    {
      id: 'r1',
      vehicleId: 'v1',
      title: 'Susi rented a car',
      summary: 'Susi rented a car for going to the beach',
      dueDate: '2025-04-01'
    },
    {
      id: 'r2',
      vehicleId: 'v3',
      title: 'Hansi rented a car',
      summary: 'Hansi rented a car for going to the mountains',
      dueDate: '2025-04-03'
    },
    {
      id: 'r3',
      vehicleId: 'v3',
      title: 'Berta rented a car',
      summary: 'Berta rented also a car for going to the mountains',
      dueDate: '2025-04-07'
    },
  ];

  get selectedVehicleRentals() {
    return this.rentals.filter((rental) => rental.vehicleId === this.vehicleId);
  }

  onCompleteRental(id: string) {
    this.rentals = this.rentals.filter((rental) => rental.id !== id);
  }

  onStartAddRental() {
    this.isAddingRental = true;
  }
}
When the user clicks on the button "Add Rental", we want to show the new component.
@if (isAddingRental) {
  <app-new-rental />
}

<section id="rentals">
  <header>
    <h2>{{ brand }}'s Rentals</h2>
    <menu>
      <button (click)="onStartAddRental()">Add Rental</button>
    </menu>
  </header>

  <ul>
    @for (rental of selectedVehicleRentals; track rental.id) {
      <li>
        <app-rental [rental]="rental" (complete)="onCompleteRental($event)"/>
      </li>
    }
  </ul>
</section>
vhcl 035 result

4.27. Managing the "New Rental" dialogue

html and css for the new-rental component
<div class="backdrop"></div>
<dialog open>
  <h2>Add Rental</h2>
  <form>
    <p>
      <label for="title">Title</label>
      <input type="text" id="title" name="title" />
    </p>

    <p>
      <label for="summary">Summary</label>
      <textarea id="summary" rows="5" name="summary"></textarea>
    </p>

    <p>
      <label for="due-date">Due Date</label>
      <input type="date" id="due-date" name="due-date" />
    </p>

    <p class="actions">
      <button type="button">Cancel</button>
      <button type="submit">Create</button>
    </p>
  </form>
</dialog>
.backdrop {
  background-color: rgba(0, 0, 0, 0.9);
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
}

dialog {
  width: 90%;
  max-width: 30rem;
  background-color: #433352;
  border-radius: 6px;
  border: none;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
  overflow: hidden;
  padding: 1rem;
  top: 5rem;
}

h2 {
  margin: 0;
  color: #d0c2e1;
}

label {
  display: block;
  font-weight: bold;
  font-size: 0.85rem;
  color: #ab9ac0;
}

input,
textarea {
  width: 100%;
  font: inherit;
  padding: 0.15rem 0.25rem;
  border-radius: 4px;
  border: 1px solid #ab9ac0;
  background-color: #d0c2e1;
}

.actions {
  margin: 1rem 0 0;
  display: flex;
  justify-content: flex-end;
  gap: 0.25rem;
}

button {
  font: inherit;
  cursor: pointer;
  border: none;
  padding: 0.35rem 1.25rem;
  border-radius: 4px;
  background-color: transparent;
}

button[type="button"] {
  color: #bdadcf;
}

button[type="button"]:hover,
button[type="button"]:active {
  color: #d0c2e1;
}

button[type="submit"] {
  background-color: #9c73ca;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: all 0.3s ease;
}

button[type="submit"]:hover,
button[type="submit"]:active {
  background-color: #895cce;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.3);
}

@media (min-width: 768px) {
  dialog {
    padding: 2rem;
  }
}
vhcl 036 result
  • There is no functionality yet, but the dialog is displayed when the user clicks on the button "Add Rental".

4.27.1. Closing the "New Rental" dialogue

  • Either the user clicks on the button "Cancel" or the user clicks outside the dialog (the backdrop).

new-rental.component.ts
import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-new-rental',
  imports: [],
  templateUrl: './new-rental.component.html',
  styleUrl: './new-rental.component.css'
})
export class NewRentalComponent {
  @Output() cancel = new EventEmitter<void>();

  onCancel() {
    this.cancel.emit();
  }
}
  • We add an output property to the new-rental component and a method to emit an event when the user clicks on the button "Cancel".

new-rental.component.html
<div class="backdrop" (click)="onCancel()"></div>
<dialog open>
  <h2>Add Rental</h2>
  <form>
    <p>
      <label for="title">Title</label>
      <input type="text" id="title" name="title" />
    </p>

    <p>
      <label for="summary">Summary</label>
      <textarea id="summary" rows="5" name="summary"></textarea>
    </p>

    <p>
      <label for="due-date">Due Date</label>
      <input type="date" id="due-date" name="due-date" />
    </p>

    <p class="actions">
      <button type="button" (click)="onCancel()">Cancel</button>
      <button type="submit">Create</button>
    </p>
  </form>
</dialog>
  • Now we add a click-handler to the backdrop, so the user can also close the dialog by clicking outside the dialog.

  • We add a click-handler to the button "Cancel", so the user can close the dialog by clicking on the button "Cancel".

4.28. Using Directives & Two-Way-Binding

  • We can use the ngModel directive to bind the input fields to properties in the component.

  • Directives are used to add behavior to an existing DOM element or component. They can be structural (like ngFor and ngIf) or attribute directives (like ngModel).

  • Directives are similar to components, but they do not have their own view or template. Instead, they modify the behavior or appearance of an existing element.

  • Directives in Angular : A Complete Guide

vhcl 037 directives slide
Figure 4. from M. Schwarzmüller - Angular - The Complete Guide (2025 Edition) - Udemy
  • Now we can use the ngModel directive to bind the input fields to properties in the component.

  • To show how it works, we add a new property "Title 2" to the NewRentalComponent and bind the input fields to this property.

<div class="backdrop" (click)="onCancel()"></div>
<dialog open>
  <h2>Add Rental</h2>
  <form>
    <p>
      <label for="title">Title</label>
      <input type="text" id="title" name="title" [(ngModel)]="enteredTitle" />
    </p>

    <p>
      <label for="title2">Title 2</label>
      <input type="text" id="title2" name="title2" [(ngModel)]="enteredTitle" />
    </p>

    <p>
      <label for="summary">Summary</label>
      <textarea id="summary" rows="5" name="summary"></textarea>
    </p>

    <p>
      <label for="due-date">Due Date</label>
      <input type="date" id="due-date" name="due-date" />
    </p>

    <p class="actions">
      <button type="button" (click)="onCancel()">Cancel</button>
      <button type="submit">Create</button>
    </p>
  </form>
</dialog>
vhcl 038 result

4.29. Handling Form Submission

  • The form button would now try to send the form data to the server, but we don’t have a server yet.

  • so we have to deal with the data on the client-side.

  • In Angular this behaviour will be automatically, because we imported FormsModule to get the ngModel.

    • FormsModule includes a component which uses the standard form-element-tag as a selector and therefore takes control of this form.

    • This form component will automatically listen to the submit event and prevent the default behaviour of the form.

    • With ngSubmit we will get the submit event and can handle it in our component.

new-rental.component.html
<div class="backdrop" (click)="onCancel()"></div>
<dialog open>
  <h2>Add Rental</h2>
  <form (ngSubmit)="onSubmit()">
    <p>
      <label for="title">Title</label>
      <input type="text" id="title" name="title" [(ngModel)]="enteredTitle" />
    </p>

    <p>
      <label for="summary">Summary</label>
      <textarea id="summary" rows="5" name="summary" [(ngModel)]="enteredSummary"></textarea>
    </p>

    <p>
      <label for="due-date">Due Date</label>
      <input type="date" id="due-date" name="due-date" [(ngModel)]="enteredDate" />
    </p>

    <p class="actions">
      <button type="button" (click)="onCancel()">Cancel</button>
      <button type="submit">Create</button>
    </p>
  </form>
</dialog>
new-rental.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Component, EventEmitter, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-new-rental',
  imports: [FormsModule],
  templateUrl: './new-rental.component.html',
  styleUrl: './new-rental.component.css'
})
export class NewRentalComponent {
  @Output() cancel = new EventEmitter<void>()
  @Output() add = new EventEmitter<{ title: string; summary: string; date: string }>()
  enteredTitle = ''
  enteredSummary = ''
  enteredDate = ''

  onCancel() {
    this.cancel.emit()
  }

  onSubmit() {
    this.add.emit({
      title: this.enteredTitle,
      summary: this.enteredSummary,
      date: this.enteredDate,
    })
  }
}
  • rentals-component.ts soll nun den Event add abfangen.

  • The method would look like this:

rentals.component.ts
onAddTask(rentalData: { title: string; summary: string; date: string }) {
  // ...
}
  • Wir führen für rentalData ein neues Interface ein, um die Daten zu typisieren.:

rental-model.ts
export interface Rental {
  id: string
  vehicleId: string
  title: string
  summary: string
  dueDate: string
}

export interface NewRentalData {
  title: string
  summary: string
  date: string
}
  • You could create a separate file new-rental-model.ts, but we will use rental-model.ts.

  • The files should now look like:

new-rental.component.html
<div class="backdrop" (click)="onCancel()"></div>
<dialog open>
  <h2>Add Rental</h2>
  <form (ngSubmit)="onSubmit()">
    <p>
      <label for="title">Title</label>
      <input type="text" id="title" name="title" [(ngModel)]="enteredTitle" />
    </p>

    <p>
      <label for="summary">Summary</label>
      <textarea id="summary" rows="5" name="summary" [(ngModel)]="enteredSummary"></textarea>
    </p>

    <p>
      <label for="due-date">Due Date</label>
      <input type="date" id="due-date" name="due-date" [(ngModel)]="enteredDate" />
    </p>

    <p class="actions">
      <button type="button" (click)="onCancel()">Cancel</button>
      <button type="submit">Create</button>
    </p>
  </form>
</dialog>
new-rental.component.ts
import { Component, EventEmitter, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { type NewRentalData } from '../rental/rental.model';

@Component({
  selector: 'app-new-rental',
  imports: [FormsModule],
  templateUrl: './new-rental.component.html',
  styleUrl: './new-rental.component.css'
})
export class NewRentalComponent {
  @Output() cancel = new EventEmitter<void>()
  @Output() add = new EventEmitter<NewRentalData>()
  enteredTitle = ''
  enteredSummary = ''
  enteredDate = ''

  onCancel() {
    this.cancel.emit()
  }

  onSubmit() {
    this.add.emit({
      title: this.enteredTitle,
      summary: this.enteredSummary,
      date: this.enteredDate,
    })
  }
}
rentals.component.html
@if (isAddingRental) {
  <app-new-rental
    (cancel)="onCancelAddTask()"
    (add)="onAddTask($event)"
  />
}

<section id="rentals">
  <header>
    <h2>{{ brand }}'s Rentals</h2>
    <menu>
      <button (click)="onStartAddRental()">Add Rental</button>
    </menu>
  </header>

  <ul>
    @for (rental of selectedVehicleRentals; track rental.id) {
      <li>
        <app-rental [rental]="rental" (complete)="onCompleteRental($event)"/>
      </li>
    }
  </ul>
</section>
rentals.component.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';
import { NewRentalComponent } from './new-rental/new-rental.component';
import { type NewRentalData } from './rental/rental.model';

@Component({
  selector: 'app-rentals',
  imports: [RentalComponent, NewRentalComponent],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {
  @Input({ required: true }) vehicleId!: string
  @Input({ required: true }) brand!: string
  isAddingRental = false;

  rentals = [
    {
      id: 'r1',
      vehicleId: 'v1',
      title: 'Susi rented a car',
      summary: 'Susi rented a car for going to the beach',
      dueDate: '2025-04-01'
    },
    {
      id: 'r2',
      vehicleId: 'v3',
      title: 'Hansi rented a car',
      summary: 'Hansi rented a car for going to the mountains',
      dueDate: '2025-04-03'
    },
    {
      id: 'r3',
      vehicleId: 'v3',
      title: 'Berta rented a car',
      summary: 'Berta rented also a car for going to the mountains',
      dueDate: '2025-04-07'
    },
  ];

  get selectedVehicleRentals() {
    return this.rentals.filter((rental) => rental.vehicleId === this.vehicleId)
  }

  onCompleteRental(id: string) {
    this.rentals = this.rentals.filter((rental) => rental.id !== id)
  }

  onStartAddRental() {
    this.isAddingRental = true
  }

  onCancelAddTask() {
    this.isAddingRental = false
  }

  onAddTask(rentalData: NewRentalData) {
    this.rentals.unshift({
      id: new Date().getTime().toString(),
      vehicleId: this.vehicleId,
      title: rentalData.title,
      summary: rentalData.summary,
      dueDate: rentalData.date
    })
    this.isAddingRental = false
  }
}
  • At line 4 we use type for better performance when compiling and for getting no problems with circular dependencies.

  • at line 58 we could use push for adding the new rental at the end of the list, but we want to add the new rental at the beginning of the list, so we use unshift.

  • Now you can try the result

4.30. Content Projection with ngContent

  • To get rounded corners when displaying rentals you could use the ng-container directive.

ng g c shared/card --skip-tests
result
Node.js version v23.11.0 detected.
CREATE src/app/shared/card/card.component.css (0 bytes)
CREATE src/app/shared/card/card.component.html (19 bytes)
CREATE src/app/shared/card/card.component.ts (206 bytes)
  • CUT from vehicle.component.css this style and add it to the card.component.css

card.component.css
div {
  border-radius: 6px;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}
vehicle.component.html
<app-card>
  <div>
    <button [class.active]="selected"
            (click)="onSelectVehicle()">
      <img
        [src]="imagePath"
        [alt]="vehicle.brand"
      />
      <span>{{ vehicle.brand }}</span>
    </button>
  </div>
</app-card>
  • You have to import the CardComponent (app-card) into the vehicle-component.ts.

vhcl 039 import
card.component.html
<div>
  <ng-content />
</div>
We are using here the directive <ng-content> in the card.component.html. The <ng-content>-directive is a placeholder for the content that will be projected into the card component.
rental.component.html
<app-card>
  <article>
    <h2>{{ rental.title }}</h2>
    <time>{{ rental.dueDate }}</time>
    <p>{{ rental.summary }}</p>
    <p class="actions">
      <button (click)="onCompleteRental()">Complete</button>
    </p>
  </article>
</app-card>
to import the CardComponent (app-card) into the rental-component.ts.
vhcl 039 import
  • Now are the rentals inside the vehicles also provided with rounded corners.

4.31. Transforming Template Data with Pipes

  • At the moment the dueDate in rental.component.html is displayed in the format YYYY-MM-DD. We want to display it in the format DD.MM.YYYY.

<app-card>
    ...
    <time>{{ rental.dueDate }}</time>
    ...
</app-card>

is rendered to

vhcl 041 date rendering
  • We can use the date pipe to format the date.

  • The date pipe is a built-in Angular pipe that formats a date value according to locale rules. It can be used to format dates in various ways, such as displaying the date in a specific format or converting it to a different time zone.

rental.component.html
<app-card>
  <article>
    <h2>{{ rental.title }}</h2>
    <time>{{ rental.dueDate | date }}</time>
    <p>{{ rental.summary }}</p>
    <p class="actions">
      <button (click)="onCompleteRental()">Complete</button>
    </p>
  </article>
</app-card>
Don’t forget to import the date-pipe into the rental-component.ts.
vhcl 042 import date pipe
  • It is possible to customize the date

vhcl 043 date pipe doc

4.32. Getting Started with Services

  • Now we have to solve some structural problems. It is not a good idea to have the data in the rentals-component. We should use a service to manage the data.

  • First we create a file rentals.service.ts in the rentals folder.

vhcl 044 create service
  • Then we move code from rentals.component.ts to the service rentals.service.ts.

rentals.service.ts
import { type NewRentalData } from './rental/rental.model';

class RentalsService {

  private rentals = [
    {
      id: 'r1',
      vehicleId: 'v1',
      title: 'Susi rented a car',
      summary: 'Susi rented a car for going to the beach',
      dueDate: '2025-04-01'
    },
    {
      id: 'r2',
      vehicleId: 'v3',
      title: 'Hansi rented a car',
      summary: 'Hansi rented a car for going to the mountains',
      dueDate: '2025-04-03'
    },
    {
      id: 'r3',
      vehicleId: 'v3',
      title: 'Berta rented a car',
      summary: 'Berta rented also a car for going to the mountains',
      dueDate: '2025-04-07'
    },
  ];

  getVehicleRentals(vehicleId: string) {
    return this.rentals.filter(
      (rental) => rental.vehicleId === vehicleId
    )
  }

  addRental(rentalData: NewRentalData, vehicleId: string) {
    this.rentals.unshift({
      id: new Date().getTime().toString(),
      vehicleId: vehicleId,
      title: rentalData.title,
      summary: rentalData.summary,
      dueDate: rentalData.date
    })
  }

  removeRental(rentalId: string) {
    this.rentals = this.rentals.filter((rental) => rental.id !== rentalId)
  }
}
rentals.component.ts
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';
import { NewRentalComponent } from './new-rental/new-rental.component';
import { type NewRentalData } from './rental/rental.model';

@Component({
  selector: 'app-rentals',
  imports: [RentalComponent, NewRentalComponent],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {
  @Input({ required: true }) vehicleId!: string
  @Input({ required: true }) brand!: string
  isAddingRental = false;



  get selectedVehicleRentals() {
    return
  }

  onCompleteRental(id: string) {

  }

  onStartAddRental() {
    this.isAddingRental = true
  }

  onCancelAddTask() {
    this.isAddingRental = false
  }

  onAddTask(rentalData: NewRentalData) {

    this.isAddingRental = false
  }
}
  • At the moment the app is NOT working, because we have to inject the service into the component.

4.33. Getting Started with Dependency Injection

  • First we export the service-class in the rentals.service.ts file.

rentals.service.ts
import { type NewRentalData } from './rental/rental.model';
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class RentalsService {

    // ommitted for brevity

}
rentals.component.ts
import { Component, Input } from '@angular/core';
import { RentalComponent } from './rental/rental.component';
import { NewRentalComponent } from './new-rental/new-rental.component';
import { type NewRentalData } from './rental/rental.model';
import { RentalsService } from './rentals.services';

@Component({
  selector: 'app-rentals',
  imports: [RentalComponent, NewRentalComponent],
  templateUrl: './rentals.component.html',
  styleUrl: './rentals.component.css'
})
export class RentalsComponent {
  @Input({ required: true }) vehicleId!: string
  @Input({ required: true }) brand!: string
  isAddingRental = false

  // private stores the rentalsService automatically in a local Property
  constructor(private rentalsService: RentalsService) {}

  get selectedVehicleRentals() {
    return this.rentalsService.getVehicleRentals(this.vehicleId)
  }

  onCompleteRental(id: string) {

  }

  onStartAddRental() {
    this.isAddingRental = true
  }

  onCancelAddTask() {
    this.isAddingRental = false
  }

  onAddTask(rentalData: NewRentalData) {
    this.isAddingRental = false
  }
}