In dieser Anleitung erstellst du einen Kalender zum Buchen von Besprechungsräumen: eine Timeline mit einer Zeile für jeden Raum, Buchungen, die du an ihren Platz ziehst, eine Regel, die verhindert, dass ein Raum doppelt gebucht wird, und eine Datei, die eine Buchung in jeden Kalender bringt. Dafür nutzt du den Terminplaner aus @needless-ui/angular, der wie ganz Needless UI MIT-lizenziert ist. Die Ressourcen-Timeline ist also für jede App kostenlos.

Räume und Buchungen

Räume sind Ressourcen: eine id, ein title und alles Weitere in data. Buchungen sind Termine mit einfachen ISO-Zeiten wie 2026-10-23T09:00 und der ID ihres Raums in resource. Eine Buchung kann als Liste auch mehrere Räume oder Personen enthalten.

Eine Zeile für jeden Raum

Übergib beides mit view="timeline" an nui-scheduler. dayStart und dayEnd beschränken den Tag auf die Öffnungszeiten, snapMinutes verschiebt Buchungen in Viertelstundenschritten, und businessHours schattiert die Zeit, in der die Räume geschlossen sind. nuiSchedulerResource zeichnet die Kopfzelle jeder Zeile.

booking.html
<nui-scheduler
[events]="bookings()"
[resources]="rooms"
view="timeline"
dayStart="8"
dayEnd="19"
snapMinutes="15"
[businessHours]="hours"
editable
selectable
[allow]="free"
(slotSelect)="book($event)"
(eventChange)="move($event)"
(rangeChange)="range.set($event)"
>
<ng-template nuiSchedulerResource let-room>{{ room.title }}</ng-template>
</nui-scheduler>
<button nuiButton type="button" (click)="download()">Add to calendar</button>

Buchen und verschieben

Der Terminplaner ändert deine Daten nie selbst: Er meldet, was die Leute tun. Ziehst du über die Zeile eines Raums, markierst du Zeit, und slotSelect meldet sie samt Raum. Ziehst du eine Buchung oder ihr Ende, meldet eventChange ihre neuen Zeiten und, wenn sie in einen anderen Raum gewandert ist, auch den neuen Raum. Aktualisiere das Signal, und die Timeline folgt.

booking.ts
import { Component, signal } from '@angular/core';
import { NuiButton } from '@needless-ui/angular/button';
import {
NuiScheduler,
type NuiSchedulerChange,
type NuiSchedulerEvent,
NuiSchedulerResourceTemplate,
type NuiSchedulerSlot,
} from '@needless-ui/angular/scheduler';
import { downloadIcs } from './booking-ics';

@Component({
selector: 'app-bookings',
imports: [NuiButton, NuiScheduler, NuiSchedulerResourceTemplate],
templateUrl: './booking.html',
})
export class Bookings {
/** Rooms are resources; bookings are events, with their room in `resource`. */
protected readonly rooms = [
{ id: 'atlas', title: 'Atlas' },
{ id: 'boreal', title: 'Boreal' },
];
protected readonly bookings = signal<NuiSchedulerEvent[]>([]);
/** Open on weekdays from 8:00 to 19:00: other time is shaded. */
protected readonly hours = [{ days: [1, 2, 3, 4, 5], start: '08:00', end: '19:00' }];
/** The days on screen: load their bookings when it changes. */
protected readonly range = signal<{ start: string; end: string } | null>(null);

protected book({ start, end, resource }: NuiSchedulerSlot): void {
const booking = { id: crypto.randomUUID(), title: 'New booking', start, end, resource };
this.bookings.update((all) => [...all, booking]);
}

protected move({ event, start, end, resource }: NuiSchedulerChange): void {
this.bookings.update((all) =>
all.map((b) =>
b.id === event.id ? { ...b, start, end, resource: resource ?? b.resource } : b,
),
);
}

/** A room holds one booking at a time. */
protected readonly free = (time: NuiSchedulerChange | NuiSchedulerSlot): boolean => {
const id = 'event' in time ? time.event.id : null;
const room = 'event' in time ? (time.resource ?? time.event.resource) : time.resource;
return !this.bookings().some(
(b) => b.id !== id && b.resource === room && b.start < time.end && b.end > time.start,
);
};

protected download(): void {
downloadIcs(this.bookings(), 'bookings');
}
}

Keine Doppelbuchungen

allow wird gefragt, bevor eine Buchung angelegt oder verschoben wird. Gibt es false zurück, erscheint das Ziehen rot und legt nichts ab, und per Tastatur sagt der Terminplaner an, dass die Zeit nicht verfügbar ist. Das Beispiel prüft die anderen Buchungen des Raums; prüfe auf deinem Server noch einmal nach, denn er hat das letzte Wort.

Angezeigte Tage laden

rangeChange meldet den ersten und letzten angezeigten Tag, einmal zu Beginn und erneut bei jedem Blättern, damit du nur diese Buchungen abrufen kannst, etwa mit httpResource oder einer beliebigen anderen Anfrage.

Zum Kalender hinzufügen

nuiSchedulerToIcs() schreibt Buchungen als iCalendar-Datei, die Google Calendar, Outlook und Apple Calendar importieren. nuiSchedulerFromIcs() liest eine solche Datei wieder ein und rechnet Zeiten aus anderen Zeitzonen in die Zeitzone des Lesers um.

booking-ics.ts
import { type NuiSchedulerEvent, nuiSchedulerToIcs } from '@needless-ui/angular/scheduler';

/** Saves events as an .ics file, which Google Calendar, Outlook and Apple Calendar import. */
export function downloadIcs(events: readonly NuiSchedulerEvent[], name: string): void {
const file = new Blob([nuiSchedulerToIcs(events, { name })], { type: 'text/calendar' });
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.download = `${name}.ics`;
link.click();
URL.revokeObjectURL(link.href);
}

Tastatur und Screenreader

Alles funktioniert ohne Maus. Die Timeline ist ein einziger Tab-Stopp: Mit den Pfeiltasten bewegst du dich durch Uhrzeiten und Räume, Umschalt plus Pfeiltaste markiert Zeit, und Enter bucht sie. Auf einer Buchung verschiebst du sie mit Alt plus Pfeiltaste auf eine andere Zeit oder in einen anderen Raum. Jede Buchung ist ein Button, benannt nach Titel, Raum, Tag und Uhrzeit.

Für die Räume nebeneinander in der Tagesansicht, wiederkehrende Buchungen und alle weiteren Optionen sieh dir die Seite des Terminplaners und seine API an.