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',
  standalone: true,
  templateUrl: './header.component.html'
})
export class HeaderComponent {
}
header.component.html
<header>
  <h1>EasyRent</h1>
</header>
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

Wir wollen die Komponenten hierarchisch aufbauen, daher soll der header in der app-component sein.
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>
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;
  }
}
  • copy the file rent-a-car-logo-2.png into the public-folder

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
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;
}
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
<app-header />
<app-vehicle />
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: 'Remault 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.
vehicle.component.html (String Interpolation)
<div>
  <button>
    <img src="icons/"/>
    <span>{{ selectedVehicle.brand }}</span>
  </button>
</div>
vehicle.component.html (Property Binding)
<div>
  <button>
    <img
      [src]="'icons/' + selectedVehicle.avatar"
      [alt]="selectedVehicle.brand"
    />
    <span>{{ selectedVehicle.brand }}</span>
  </button>
</div>

4.1. 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.2. Listening to Events With Event Binding

vhcl 010 create method
vehicle.component.html
<div>
  <button (click)="onSelectUser()">
    <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;
  }

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

4.3. Managing State & Changing Data

  • Nun wollen die UI mittels Button-Click andern — also den Zustand (State)

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;
  }

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

4.4. Angular’s Change Detection Mechanism

vhcl 012 signals vs zonejs

4.5. Introducing Signals

vhcl 013 signals
vehicle.component.html
<div>
  <button (click)="onSelectUser()">
    <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
vehicle.component.ts