Dans ce guide, vous allez créer un calendrier pour réserver des salles de réunion : une timeline avec une ligne par salle, des réservations que vous faites glisser à leur place, une règle qui empêche de réserver deux fois la même salle, et un fichier qui ajoute une réservation à n’importe quel calendrier. L’ensemble repose sur le planificateur de @needless-ui/angular, sous licence MIT comme tout Needless UI : la timeline de ressources est donc gratuite pour toute application.
Salles et réservations
Les salles sont des ressources : un id, un title, et tout le reste dans data. Les réservations sont des événements avec de simples heures au format ISO, comme 2026-10-23T09:00, et l’id de leur salle dans resource. Une réservation peut porter sur plusieurs salles, ou personnes, sous forme de liste.
Une ligne par salle
Passez les salles et les réservations à nui-scheduler avec view="timeline". dayStart et dayEnd limitent la journée aux heures d’ouverture, snapMinutes déplace les réservations par quarts d’heure, et businessHours grise le temps où les salles sont fermées. nuiSchedulerResource dessine l’en-tête de chaque ligne.
<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>Réserver et déplacer
Le planificateur ne modifie jamais vos données de lui-même : il signale ce que font les gens. Glisser le long de la ligne d’une salle choisit une plage horaire, que slotSelect signale avec la salle. Faire glisser une réservation, ou sa fin, signale ses nouveaux horaires dans eventChange, et sa nouvelle salle si elle passe dans une autre. Mettez à jour le signal, et la timeline suit.
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');
}
}Pas de double réservation
allow est consulté avant qu’une réservation soit créée ou déplacée. Quand il renvoie false, le glisser-déposer s’affiche en rouge et ne dépose rien, et au clavier, le planificateur annonce que l’horaire n’est pas disponible. L’exemple vérifie les autres réservations de la salle ; vérifiez à nouveau sur votre serveur, qui a le dernier mot.
Charger les jours affichés
rangeChange signale le premier et le dernier jour affichés, une fois au départ puis à chaque changement de page : vous pouvez ainsi ne charger que ces réservations, avec httpResource ou la requête de votre choix.
Ajouter au calendrier
nuiSchedulerToIcs() écrit les réservations dans un fichier iCalendar, que Google Calendar, Outlook et Apple Calendar importent. nuiSchedulerFromIcs() en relit un, et ramène les heures d’autres fuseaux horaires à celui de la personne qui le lit.
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);
}Clavier et lecteurs d’écran
Tout fonctionne sans souris. La timeline ne compte qu’un seul arrêt de tabulation : les flèches parcourent les heures et les salles, Maj avec les flèches choisit une plage horaire, et Entrée la réserve. Sur une réservation, Alt avec les flèches la déplace vers un autre horaire ou une autre salle. Chaque réservation est un bouton nommé par son titre, sa salle, son jour et son horaire.
Pour la vue jour avec les salles côte à côte, les réservations récurrentes et toutes les autres options, consultez la page du planificateur et son API.