Zum Inhalt springen
SuperScheduler

GrundbegriffeGilt fürSuperScheduler Pro

Kontrollierte Ereignisse und Callbacks

Übergeben Sie die Ereignisse aus dem React-State und schreiben Sie sie in onEventsChange zurück. Das Control ruft diesen Callback einmal pro Task nach einem Ablegen, einer Dauer-Änderung oder einem Aufruf von control.events auf, mit der neuen Liste, den geänderten und entfernten Objekten und einem Grund. Geben Sie dem Control eine Kopie Ihres Arrays, denn es übernimmt das Array und bearbeitet es direkt. Die Bibliothek spricht nie mit Ihrem Backend: Speichern Sie in onEventMove, um vor dem Übernehmen der Änderung zu bestätigen, oder in onEventsChange, um optimistisch zu speichern und bei einem Fehler zurückzusetzen.

Geprüft mit v0.1.0 · überarbeitet am 7. Oktober 2026.md

SuperScheduler Pro kennt zwei Arten, Ereignisdaten zu besitzen. Kontrolliert: Der React-State ist die maßgebliche Quelle, Sie übergeben ihn als events, und onEventsChange teilt Ihnen mit, was der Nutzer oder die API geändert hat. Unkontrolliert: Sie übergeben Anfangsdaten mit defaultEvents, und das Control führt seine eigene Liste. Kontrolliert ist der richtige Standard für eine Anwendung, die Änderungen speichert, sie an anderer Stelle der Seite anzeigt oder Rückgängig machen unterstützt.

Diese Seite erklärt das kontrollierte Muster, was der Änderungs-Callback erhält, die Regeln zum Besitz des Arrays, auf denen alles beruht, die genaue Reihenfolge der Callbacks beim Ablegen und an welcher Stelle Ihr Backend ins Spiel kommt.

Das kontrollierte Muster

src/ControlledPlanning.tsxtsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SuperScheduler } from 'super-scheduler'

const ROOMS: SuperScheduler.ResourceData[] = [
  { id: 'r101', name: 'Room 101' },
  { id: 'r102', name: 'Room 102' },
]
const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Month' },
  { groupBy: 'Day', format: 'd' },
]

const INITIAL: SuperScheduler.EventData[] = [
  {
    id: 'b-1042',
    resource: 'r101',
    start: '2026-10-02T14:00:00',
    end: '2026-10-05T11:00:00',
    text: 'Booking 1042',
  },
  {
    id: 'b-1043',
    resource: 'r102',
    start: '2026-10-03T14:00:00',
    end: '2026-10-08T11:00:00',
    text: 'Booking 1043',
  },
]

export function ControlledPlanning() {
  // React state is the single source of truth for the events.
  const [events, setEvents] = useState<SuperScheduler.EventData[]>(INITIAL)

  // The control adopts the array it receives and splices it in place: give it its own copy.
  const owned = useMemo(() => events.slice(), [events])

  // Once per task, after a drop, a resize or a control.events call. Handing the same objects back
  // is recognised as an echo: the control does not reload or repaint.
  const onEventsChange = useCallback((args: SchedulerEventsChangeArgs) => {
    setEvents([...args.events])
  }, [])

  // Changes made outside the scheduler go to state; the control picks up the new array.
  const addBlock = () =>
    setEvents((current) => [
      ...current,
      {
        id: `block-${crypto.randomUUID()}`,
        resource: 'r102',
        start: '2026-10-12T00:00:00',
        end: '2026-10-14T00:00:00',
        text: 'Maintenance',
        moveDisabled: true,
        resizeDisabled: true,
      },
    ])

  return (
    <>
      <p>
        {events.length} events{' '}
        <button type="button" onClick={addBlock}>
          Block Room 102
        </button>
      </p>
      <SuperSchedulerComponent
        startDate="2026-10-01"
        days={31}
        scale="Day"
        timeHeaders={TIME_HEADERS}
        resources={ROOMS}
        events={owned}
        onEventsChange={onEventsChange}
      />
    </>
  )
}

Sie sollten zwei Buchungen und einen Ereigniszähler sehen. Ziehen Sie eine Buchung in das andere Zimmer: Sie bleibt dort, wo Sie sie abgelegt haben, weil ihre neue Position jetzt im React-State steht. Drücken Sie den Button: Der Zähler steigt, und auf Room 102 erscheint ein Wartungsblock, gesperrt gegen Verschieben und Dauer ändern.

Drei Zeilen tragen das Muster:

  1. useState hält die Ereignisse. Alles, was sie anzeigt oder bearbeitet, liest diesen State.
  2. useMemo(() => events.slice(), [events]) gibt dem Control seine eigene Kopie des Arrays (siehe Besitz des Arrays).
  3. onEventsChange schreibt die neue Liste des Controls mit setEvents([...args.events]) in den State zurück.

Ändert sich der State aus einem anderen Grund (ein Formular, ein Server-Push, der Button oben), erreicht das neue Array das Control als geänderte Prop, und das Control lädt es neu.

Was onEventsChange erhält

onEventsChange wird aufgerufen, nachdem sich der Ereignisspeicher des Controls geändert hat, und zwar höchstens einmal pro Task: Mehrere Änderungen im selben synchronen Block kommen gemeinsam in einem Aufruf an, im nächsten Microtask.

ArgumentInhalt
eventsDie vollständige Liste des Controls nach der Änderung, als Datenobjekte
changedDurch diese Änderung hinzugefügte oder ersetzte Objekte, in ihrem neuen Zustand
removedDurch diese Änderung entfernte oder ersetzte Objekte, in ihrem vorherigen Zustand
reasonWarum sich der Speicher geändert hat (siehe unten)
reasonAusgelöst durch
'move'Ein abgeschlossenes Drag-and-drop, auch per Tastatur, sowie Ablegen von außerhalb des Planers
'resize'Eine abgeschlossene Dauer-Änderung
'create'control.events.add()
'update'control.events.update() mit einem neuen Objekt oder ein Hinzufügen und ein Entfernen im selben Task
'remove'control.events.remove(), einschließlich des eingebauten Lösch-Buttons (eventDeleteHandling: 'Update')
'history'Rückgängig machen oder Wiederholen, vom Control über super-scheduler/history angewendet
'load'Sie haben andere Ereignisobjekte als events übergeben, oder ein Bereichslader hat neu geladene Ereignisse eingefügt
'api'Andere Änderungen, die die Bibliothek von sich aus am Speicher vornimmt; behandeln Sie sie wie 'update'

Bei einer Verschiebung enthält changed das neue Objekt und removed das Objekt, das es ersetzt hat: Sie haben den Zustand vorher und nachher, ohne eine eigene Kopie zu führen. Die Objekte in events behalten ihre Identität zwischen den Aufrufen, sofern sie sich nicht geändert haben; React.memo und Selektoren, die per Referenz vergleichen, funktionieren also weiter.

Unkontrolliert: defaultEvents

Übergeben Sie defaultEvents statt events, wenn der Planer die Daten besitzen darf, etwa in einer überwiegend lesenden Ansicht oder einem Prototyp. Das Array wird einmal gelesen, bei der Initialisierung; spätere Änderungen der Prop werden mit einer Warnung in der Entwicklung ignoriert. Übergeben Sie beide, gewinnt events, ebenfalls mit einer Warnung.

In diesem Modus lesen Sie die aktuellen Daten aus control.events.list, abonnieren sie mit useScheduler({ track: ['events'] }) aus super-scheduler/hooks oder hören weiterhin auf onEventsChange, das in beiden Modi funktioniert.

Das Control übernimmt Ihr Array

Aus Geschwindigkeitsgründen kopiert das Control das Array, das Sie als events übergeben, nicht: control.events.list ist dieses Array, und Hinzufügen, Entfernen und Ablegen bearbeiten es direkt mit splice. Deshalb übergibt das Muster oben eine Kopie. Ohne die Kopie würde das Control das Array in Ihrem React-State verändern, an React vorbei.

Dieselbe Regel erklärt das übrige Verhalten der kontrollierten Schleife:

  • Echos kosten nichts. Speichert onEventsChange [...args.events], rendert React, und das Control erhält ein Array mit genau den Objekten, die es bereits hält. Es erkennt das Echo und tut nichts: kein Neuladen, kein Neuzeichnen.
  • Neue Objekte lösen ein Neuladen aus. Enthält Ihr State Objekte, die das Control noch nicht gesehen hat (eine Bearbeitung im Formular, eine Serverantwort), lädt es seine Liste aus dem neuen Array neu und meldet dann reason: 'load'. Diese Liste erneut zu speichern ist ein Echo, und die Schleife endet dort.
  • Frieren Sie das Array nicht ein, wenn Sie control.events.add, update oder remove aufrufen: Sie bearbeiten es direkt und werfen bei einem eingefrorenen Array einen TypeError. Ablegen und Dauer ändern kopieren ein eingefrorenes Array vorher.

Reihenfolge der Callbacks beim Ablegen

Jedes Drag-and-drop durchläuft eine feste Abfolge. Dieser Logger macht sie sichtbar:

src/tracing.tsts
import type { SchedulerProps } from 'super-scheduler'

// Logs every callback of one drag-and-drop, in the order the library calls them.
export const tracing: SchedulerProps = {
  onEventMoving: (args) =>
    console.debug('1. moving (every shadow change)', args.start.value, args.allowed),
  onEventMove: (args) =>
    console.debug('2. move (before the commit, cancelable)', args.newStart.value),
  onEventMoved: (args) =>
    // The store already holds the new times here.
    console.debug(
      '3. moved (after the commit)',
      args.control.events.find(args.e.id())?.start().value,
    ),
  onEventsChange: (args) =>
    console.debug('4. eventsChange (next microtask)', args.reason, args.changed.length),
}
  1. onEventMoving läuft bei jeder Änderung des Schattens, während der Nutzer zieht. Es kann die Position ablehnen oder anpassen (siehe Ziehen, Dauer ändern und Geschäftsregeln).
  2. Wurde beim Loslassen die letzte Position abgelehnt (durch Ihre Regel, eine Überlappung, eine gesperrte Zelle), läuft nichts weiter: kein onEventMove, keine Änderung.
  3. onEventMove läuft einmal, bevor sich der Speicher ändert. Es kann mit args.preventDefault() abbrechen, args.newStart, args.newEnd oder args.newResource ändern oder die Entscheidung mit args.async = true und args.loaded() aufschieben.
  4. Der Speicher wird aktualisiert (mit eventMoveHandling: 'Update', dem Standard).
  5. onEventMoved läuft nach dem Übernehmen: args.control.events.find(id) liefert bereits die neuen Zeiten.
  6. onEventsChange läuft im nächsten Microtask mit reason: 'move'.

Das Ändern der Dauer folgt derselben Abfolge mit onEventResizing, onEventResize, onEventResized und reason: 'resize'. Da onEventMoved läuft, bevor React etwas gespeichert hat, lesen Sie die neuen Werte aus seinen Argumenten, nicht aus Ihrem State.

Wo Ihr Backend ins Spiel kommt

Der Planer ruft nie einen Server auf. Sie entscheiden, wann gespeichert wird, und es gibt zwei solide Ansätze.

Bestätigen, bevor die Änderung übernommen wird

Speichern Sie in onEventMove oder onEventResize mit args.async = true und rufen Sie args.loaded() auf, wenn der Server antwortet; hat er abgelehnt, rufen Sie vorher args.preventDefault() auf. Bis dahin bleibt das Ereignis, wo es war, der Bildschirm zeigt also nie eine Änderung, die der Server abgelehnt hat. Der Preis ist eine sichtbare Verzögerung bei jedem Ablegen. Das vollständige Muster steht unter Beim Ablegen bestätigen.

Optimistisch speichern und bei einem Fehler zurücksetzen

Übernehmen Sie die Änderung sofort, speichern Sie im Hintergrund und setzen Sie das vorherige Objekt wieder ein, wenn das Speichern fehlschlägt. onEventsChange liefert alles, was Sie brauchen: changed ist das, was gespeichert werden muss, removed das, was wiederherzustellen ist.

src/OptimisticPlanning.tsxtsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SuperScheduler } from 'super-scheduler'

const iso = (value: SuperScheduler.DateInput) => (typeof value === 'string' ? value : value.value)

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly initial: SuperScheduler.EventData[]
}

export function OptimisticPlanning({ rooms, initial }: Props) {
  const [events, setEvents] = useState(initial)
  const owned = useMemo(() => events.slice(), [events])
  const { controlRef } = useSchedulerControl()

  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => {
      // 1. Show the change immediately.
      setEvents([...args.events])
      if (args.reason !== 'move' && args.reason !== 'resize') return

      for (const after of args.changed) {
        // The object this drop replaced: the state to restore if the server says no.
        const before = args.removed.find((item) => item.id === after.id)
        if (before === undefined || after.resource === undefined) continue

        // 2. Persist it.
        saveBooking({
          id: String(after.id),
          resource: String(after.resource),
          start: iso(after.start),
          end: iso(after.end),
        })
          // 3. Revert on failure. Matching by identity leaves a newer change of the same event alone.
          .catch(() => {
            setEvents((current) => current.map((item) => (item === after ? before : item)))
            controlRef.current?.message('The change could not be saved and was undone.')
          })
      }
    },
    [controlRef],
  )

  return (
    <SuperSchedulerComponent
      controlRef={controlRef}
      startDate="2026-10-01"
      days={31}
      scale="Day"
      resources={rooms}
      events={owned}
      onEventsChange={onEventsChange}
    />
  )
}

Ein Ablegen sollte sofort wirken. Lehnt saveBooking ab, springt die Buchung an ihren vorherigen Platz zurück, und eine Meldung erklärt den Grund. Das Zurücksetzen gleicht per Objektidentität ab; hat der Nutzer dieselbe Buchung inzwischen erneut verschoben, bleibt die neuere Änderung unangetastet.

Für welchen Ansatz Sie sich auch entscheiden, diese Aufgaben bleiben in Ihrer Anwendung:

  • Auf dem Server validieren. Regeln in onEventMoving dienen der Benutzerführung; der Server muss Überlappungen, Berechtigungen und Geschäftsregeln erneut prüfen, weil andere Nutzer und andere Clients dieselben Daten ändern.
  • Normalisieren, was Sie senden. Verschobene Ereignisse tragen SuperScheduler.Date-Werte, unberührte Ihre Strings. Siehe Werte nach dem Ziehen.
  • Die Version des Servers übernehmen. Gibt der Server ein kanonisches Objekt zurück (eine neue ID für ein angelegtes Ereignis, einen neu berechneten Preis), ersetzen Sie das Objekt im State. Das Control lädt neu und meldet reason: 'load'.
  • Rückgängig machen und Wiederholen. createHistory({ apply }) aus super-scheduler/history kann Rückgängig machen und Wiederholen auf Ihren State statt auf das Control anwenden. Siehe Rückgängig machen und Wiederholen.
  • Mehrere Bereiche. SchedulerPanes teilt eine Ereignisliste zwischen Bereichen über kontrollierte events und onEventsChange (oder defaultEvents). Siehe Bereiche und gespeicherte Ansichten.
  • Laden nach Datumsbereich. Ein Bereichslader aus super-scheduler/ranges fügt ein, was er lädt, und meldet es über onEventsChange mit reason: 'load'; übernehmen Sie diese Liste. Siehe Bereichsweises Laden.

Disposition im technischen AußendienstEin dringender Auftrag kommt herein. Finden Sie das Team, das ihn rechtzeitig übernehmen kann. Planung von SchulungsräumenDie Anmeldungen sprengen den Raum. Beide Termine wählen, sehen, was für beide frei ist, zusammen verschieben und die Ansicht behalten.

Nächste Schritte