In questa guida costruirai un calendario per prenotare le sale riunioni: una timeline con una riga per ogni sala, prenotazioni da trascinare al loro posto, una regola che impedisce di prenotare due volte la stessa sala e un file che aggiunge una prenotazione a qualsiasi calendario. Userai il pianificatore di @needless-ui/angular, con licenza MIT come tutto Needless UI, quindi la timeline delle risorse è gratuita per qualsiasi app.

Sale e prenotazioni

Le sale sono risorse: un id, un title e tutto il resto in data. Le prenotazioni sono eventi con semplici orari ISO, come 2026-10-23T09:00, e l’id della loro sala in resource. Una prenotazione può includere più sale, o persone, sotto forma di lista.

Una riga per ogni sala

Passa sale e prenotazioni a nui-scheduler con view="timeline". dayStart e dayEnd limitano la giornata all’orario di apertura, snapMinutes sposta le prenotazioni di un quarto d’ora alla volta e businessHours ombreggia le ore in cui le sale sono chiuse. nuiSchedulerResource disegna l’intestazione di ogni riga.

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>

Prenota e sposta

Il pianificatore non modifica mai i tuoi dati da solo: riporta ciò che fanno le persone. Trascinando lungo la riga di una sala si sceglie un orario, che slotSelect riporta insieme alla sala. Quando si trascina una prenotazione, o la sua fine, eventChange ne riporta i nuovi orari, e anche la nuova sala se è stata spostata in un’altra. Aggiorna il signal e la timeline si adegua.

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

Niente doppie prenotazioni

allow viene consultato prima che una prenotazione venga creata o spostata. Quando restituisce false, il trascinamento appare in rosso e al rilascio non succede nulla, e da tastiera il pianificatore annuncia che l’orario non è disponibile. L’esempio controlla le altre prenotazioni della sala; ricontrolla sul tuo server, che ha l’ultima parola.

Carica i giorni visibili

rangeChange riporta il primo e l’ultimo giorno mostrati, una volta all’inizio e poi a ogni cambio di pagina, così puoi caricare solo quelle prenotazioni, con httpResource o con qualsiasi richiesta tu preferisca.

Aggiungi al calendario

nuiSchedulerToIcs() scrive le prenotazioni in un file iCalendar, che Google Calendar, Outlook e Apple Calendar importano. nuiSchedulerFromIcs() ne legge uno e converte gli orari di altri fusi orari in quello di chi lo legge.

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

Tastiera e screen reader

Tutto funziona senza mouse. La timeline è un unico tab stop: le frecce si muovono tra orari e sale, Maiusc con le frecce sceglie un orario e Invio lo prenota. Su una prenotazione, Alt con le frecce la sposta a un altro orario o in un’altra sala. Ogni prenotazione è un pulsante con il nome dato da titolo, sala, giorno e orario.

Per la vista giorno con le sale affiancate, le prenotazioni ricorrenti e tutte le altre opzioni, consulta la pagina del pianificatore e la sua API.