Event editor
Event editor is the create-and-edit form for a single scheduler event,
covering a title, description, a date (or a start and end date range in
all-day mode), start and end time, location, notes, priority, color, and
guests to share the event with, each rendered with an existing library
field component. It's an independent piece of the
Dependencies
When to use
Reach for event editor whenever you need the exact create-and-edit form scheduler itself uses for a single event, embedded in your own dialog instead of a drawer or modal, on a dedicated "new event" page, or anywhere else a standalone event form makes sense outside a full calendar.
Leaving the event unset (or null) puts the form in create mode, where it seeds from an initial start and end time, defaulting to the next hour and one hour after that. Passing an existing event switches it to edit mode instead, so every field seeds from it and a Delete button appears in the built-in footer. It never mutates the event itself, and reads or writes nothing beyond dispatching its events (see below), so the consumer decides what a save or delete actually does to their own data.
See
Install & usage
npm install @eunomia/elements
import "@eunomia/elements/scheduler-event-editor.js";
<eun-scheduler-event-editor id="editor"></eun-scheduler-event-editor>
<script>
const editor = document.querySelector("#editor");
editor.addEventListener("euneventcreate", (event) => {
console.log(event.event); // a full EunomiaSchedulerEvent, id included
});
editor.addEventListener("euneventupdate", (event) => {
console.log(event.event);
});
editor.addEventListener("euneventdelete", (event) => {
console.log(event.eventId);
});
</script>
Set hide-actions to suppress the built-in Save/Cancel/Delete footer and
drive the form from your own UI instead, via the public requestSave()/
requestDelete() methods and the dirty/valid/isEditing getters,
exactly how eun-scheduler unifies the footer with its own drawer/modal
chrome rather than rendering two stacked footers:
const saved = editor.requestSave(); // false if invalid, turning on inline errors
if (saved) {
/* editor already dispatched euneventcreate/euneventupdate */
}
editor.requestDelete(); // dispatches euneventdelete, edit mode only
Live testing
Properties
SchedulerEventEditor <eun-scheduler-event-editor>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| locale | string | — | The locale used by the nested date and time fields. Defaults to the runtime's own |
| hide-actions | boolean | false | Hides the built-in save, cancel, and delete footer, for a host that drives the form itself |
| title-label | string | 'Title' | The label of the title field |
| description-label | string | 'Description' | The label of the description field |
| all-day-label | string | 'All day' | The label of the all day checkbox |
| date-label | string | 'Date' | The label of the date field |
| end-date-label | string | 'End date' | The label of the end date field, shown in place of the start and end time fields while all day is checked |
| start-time-label | string | 'Start time' | The label of the start time field |
| end-time-label | string | 'End time' | The label of the end time field |
| location-label | string | 'Location' | The label of the location field |
| notes-label | string | 'Notes' | The label of the notes field |
| priority-label | string | 'Priority' | The label of the priority control |
| color-label | string | 'Color' | The label of the color swatch picker |
| required-label | string | "is required" | Accessible name appended after titleLabel when the title field is left empty on submit (e.g. "Title is required"). |
| guests-label | string | 'Guests' | The label of the guest-sharing field |
| add-guest-label | string | 'Add guest' | The accessible label of the guest email input's add button |
| remove-guest-label | string | 'Remove' | A prefix combined with the guest's email to build each guest chip's delete button accessible name, such as "Remove alice@example.com" |
| save-label | string | 'Save' | The label of the built-in save button |
| cancel-label | string | 'Cancel' | The label of the built-in cancel button. Resets the form to its last-seeded values |
| delete-label | string | 'Delete' | The label of the built-in delete button. Only shown while editing an existing event |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| event | EunomiaSchedulerEvent | null | — | The event being edited. Unset, or null, puts the form in create mode instead, seeded from initialStart and initialEnd |
| initialStart | Date | — | The start time to seed a new event with, in create mode. Defaults to the next hour |
| initialEnd | Date | — | The end time to seed a new event with, in create mode. Defaults to one hour after initialStart |
| colorLabels | Record<EunomiaSchedulerEventColor, string> | { red: "Red", purple: "Purple", yellow: "Yellow", blue: "Blue", green: "Green", pink: "Pink", } | Accessible name for each color swatch button, keyed by EunomiaSchedulerEventColor. |
| dirty | boolean | — | Whether any field has changed since the last seed/save — wire into a host `eun-drawer`'s own `dirty` for its built-in unsaved-changes guard. |
| isEditing | boolean | — | Whether the form is currently editing an existing event (`event` is set) rather than creating a new one. |
| valid | boolean | — | Whether the form currently passes validation (non-empty title, a complete date/start/end, and an end strictly after start). |
Slots
| Name | Description |
|---|---|
| (default) | Unused. Present only so a wrapping drawer or modal can slot this element into their own default body slot |
Events
| Name | Type | Description |
|---|---|---|
| input | Event | |
| euneventdelete | EventDeleteEvent | Fired when the built-in delete button is activated |
| eunguestinvite | GuestInviteEvent | Fired whenever a guest email is added, so a consumer can actually send the share invite |
| euneventcreate | EventCreateEvent | Fired on a valid save, in create mode |
| euneventupdate | EventUpdateEvent | Fired on a valid save, in edit mode |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --scheduler-event-editor-gap | Sets the gap between fields |
Create mode
Leaving event unset renders a blank form (seeded from initialStart/
initialEnd, if set) with no Delete button.
<eun-scheduler-event-editor></eun-scheduler-event-editor>
Seeding a new event
initialStart and initialEnd seed a blank, create-mode form's date and
time fields, useful for prefilling from wherever the user triggered "new
event," such as a clicked time slot on a calendar. Both are Date
properties (attribute: false, like every date value elsewhere in this
library), so set them from JavaScript rather than as HTML attributes.
editor.initialStart = new Date(2026, 2, 16, 15, 30);
editor.initialEnd = new Date(2026, 2, 16, 16, 30);
Edit mode
Setting event seeds every field from it, and shows a Delete button.
editor.event = {
id: "sample-1",
title: "Design review",
description: "Walk through the new onboarding flow.",
location: "Room A",
color: "purple",
priority: "high",
start: new Date(2026, 2, 16, 14, 0),
end: new Date(2026, 2, 16, 15, 0),
};
Sharing with guests
Guests are entered as emails through the "Guests" field (Enter or the
add button), each starting out pending in event.guests, since the
editor never sends anything itself and only tracks the resulting
pending/confirmed/declined status. Adding one fires eunguestinvite
with the email (and the event's id, once editing an existing event): the
signal a consumer listens for to actually send the invite. Each guest
chip shows its status as an icon: check / hourglass / cross for
confirmed/pending/declined, in both the editor and the
editor.event = {
id: "sample-guests-1",
title: "Design review",
color: "purple",
start: new Date(2026, 2, 16, 14, 0),
end: new Date(2026, 2, 16, 15, 0),
guests: [
{ email: "amelie@example.com", status: "confirmed" },
{ email: "noah@example.com", status: "pending" },
{ email: "sofia@example.com", status: "declined" },
],
};
// Fired whenever a guest email is added from the field — send the actual
// invite from here (this library only tracks the resulting status).
editor.addEventListener("eunguestinvite", (event) => {
sendInvite(event.email, event.eventId); // your own API call
});
All-day mode
Checking "All day" swaps the start/end time fields for a start/end date
field instead, with no time of day involved. Saving sets allDay: true and
start/end at the two dates' own day boundaries.
editor.event = {
id: "trip-1",
title: "Company offsite",
color: "purple",
allDay: true,
start: new Date(2026, 2, 17),
end: new Date(2026, 2, 19), // spans 3 days
};
Without the built-in footer
For a host (like eun-scheduler itself) that drives Save/Cancel/Delete
from its own surrounding chrome instead.
<eun-scheduler-event-editor hide-actions></eun-scheduler-event-editor>
Localizing labels
Every visible label, down to the guest chip's delete button, is overridable, which is what lets the whole form read naturally in a language other than English without touching a single word of markup.
<eun-scheduler-event-editor
title-label="Titre"
description-label="Description"
all-day-label="Toute la journée"
date-label="Date"
end-date-label="Date de fin"
start-time-label="Heure de début"
end-time-label="Heure de fin"
location-label="Lieu"
notes-label="Notes"
priority-label="Priorité"
color-label="Couleur"
guests-label="Invités"
add-guest-label="Ajouter un invité"
remove-guest-label="Retirer"
save-label="Enregistrer"
cancel-label="Annuler"
delete-label="Supprimer"
></eun-scheduler-event-editor>
Custom event editor
You're not limited to eun-scheduler-event-editor's own fields or layout.
It never mutates event and only ever dispatches plain euneventcreate/
euneventupdate/euneventdelete events carrying an event/eventId
property: the exact same contract eun-calendar's own wiring in
eun-input/eun-date-picker/
eun-time-picker (just title, date, start/end time and a color, no
description/notes/priority/all-day support), and drops straight into that
same eun-calendar wiring in place of <eun-scheduler-event-editor>, no
other changes needed.
<div id="custom-editor">
<eun-input id="ce-title" label="Title" required hide-error></eun-input>
<span id="ce-title-error" role="alert" hidden>Title is required</span>
<eun-date-picker id="ce-date" label="Date"></eun-date-picker>
<div class="row">
<eun-time-picker id="ce-start" label="Start time"></eun-time-picker>
<eun-time-picker id="ce-end" label="End time"></eun-time-picker>
</div>
<label for="ce-color">Color</label>
<select id="ce-color">
<option value="blue">Blue</option>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="purple">Purple</option>
<option value="yellow">Yellow</option>
<option value="pink">Pink</option>
</select>
<div class="actions">
<eun-button id="ce-delete" variant="critical">Delete</eun-button>
<eun-button id="ce-cancel" variant="secondary">Cancel</eun-button>
<eun-button id="ce-save" variant="primary">Save</eun-button>
</div>
</div>
<script type="module">
import "@eunomia/elements/input.js";
import "@eunomia/elements/date-picker.js";
import "@eunomia/elements/time-picker.js";
import "@eunomia/elements/button.js";
const root = document.querySelector("#custom-editor");
const titleEl = root.querySelector("#ce-title");
const titleErrorEl = root.querySelector("#ce-title-error");
const dateEl = root.querySelector("#ce-date");
const startEl = root.querySelector("#ce-start");
const endEl = root.querySelector("#ce-end");
const colorEl = root.querySelector("#ce-color");
const deleteBtn = root.querySelector("#ce-delete");
let currentEvent = null; // null => create mode
function startOfDay(date) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
return start;
}
function nextHour() {
const date = new Date();
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() + 1);
return date;
}
// Call this from your own click/select handlers, the same way
// `eun-scheduler-event-editor.event = ...` seeds it — `event` here is a
// `EunomiaSchedulerEvent`, `null` for create mode.
function seed(event) {
currentEvent = event;
titleEl.value = event?.title ?? "";
dateEl.value = event ? startOfDay(event.start) : startOfDay(new Date());
startEl.value = event?.start ?? nextHour();
endEl.value = event?.end ?? new Date(nextHour().getTime() + 60 * 60 * 1000);
colorEl.value = event?.color ?? "blue";
titleErrorEl.hidden = true;
deleteBtn.hidden = !event;
}
function requestSave() {
if (!titleEl.value.trim()) {
titleErrorEl.hidden = false;
return false;
}
const start = new Date(dateEl.value);
start.setHours(startEl.value.getHours(), startEl.value.getMinutes(), 0, 0);
const end = new Date(dateEl.value);
end.setHours(endEl.value.getHours(), endEl.value.getMinutes(), 0, 0);
const event = {
id: currentEvent?.id ?? crypto.randomUUID(),
title: titleEl.value.trim(),
color: colorEl.value,
start,
end,
};
// Matches `EventCreateEvent`/`EventUpdateEvent`'s own shape (an
// `event` property, not `detail`) so this drops into the exact same
// `euneventcreate`/`euneventupdate`/`euneventdelete` listeners
// "Building your own scheduler" wires up for `eun-scheduler-event-editor`.
const saveEvent = new Event(
currentEvent ? "euneventupdate" : "euneventcreate",
{ bubbles: true },
);
saveEvent.event = event;
root.dispatchEvent(saveEvent);
seed(event);
return true;
}
root.querySelector("#ce-save").addEventListener("click", requestSave);
root
.querySelector("#ce-cancel")
.addEventListener("click", () => seed(currentEvent));
deleteBtn.addEventListener("click", () => {
if (!currentEvent) {
return;
}
const deleteEvent = new Event("euneventdelete", { bubbles: true });
deleteEvent.eventId = currentEvent.id;
root.dispatchEvent(deleteEvent);
});
</script>
<style>
#custom-editor {
display: flex;
flex-direction: column;
gap: 12px;
}
#custom-editor .row {
display: flex;
gap: 12px;
}
#custom-editor .actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
#custom-editor .actions #ce-delete {
margin-right: auto;
}
</style>
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus through the fields and footer buttons in document order |
ArrowLeft/ArrowRight |
Priority: moves and selects between options, wrapping. Color: only moves focus among swatches, browsing without picking |
Home / End |
Jumps to the first/last option in the focused group |
Enter / Space |
Activates the focused field/button/swatch/option, and is how a color actually gets picked |
Every text/date/time field, including the "All day" checkbox
(eun-checkbox) and the date/time fields it toggles between, is an
existing library component, so their keyboard behavior applies unchanged,
and each one's own Accessibility tab documents the full contract. The
color field is a real
eun-color-pickerpresets-inline mode, showing its declared presets directly as the
field with no trigger button or panel, while the priority picker is a
real eun-toggle-groupsingle mode, so their exact keyboard/ARIA contracts are documented
on their own Accessibility tabs instead of duplicated here, and this
page only covers how each is configured.
Aria attributes and rules
- The color field is
eun-color-picker's own preset swatch radiogroup: arole="radiogroup"ofrole="radio"swatch buttons, each witharia-checkedreflecting the current selection and anaria-labeltaken from its option's label text (e.g."Red"), and each its own real Tab stop (tabindex="0"), reachable one at a time byTabjust like by the arrow keys. Browsing with the arrow keys only moves focus, it never changesaria-checkedon its own, picking a color always takes an explicitEnter/Spaceor click. - The priority picker is a
eun-toggle-groupwithtype="stroke"andsingleset: exclusive selection (exactly one of the four always checked, defaulting tomedium) rendered as standalone bordered buttons instead of the color picker's swatches, each aeun-toggle-itemlabelled"Low"/"Medium"/"High"/"Critical".type="stroke"is purely visual: the underlying role/state/keyboard model is identical toeun-toggle-group's default presentation, see its own Accessibility tab. Each item'svariantmapslowtosuccess,mediumto the unsetprimarydefault,hightowarning, andcriticaltocritical, a narrower, different mapping than the semantic-state token scale the read-only event chip/popover use elsewhere (that one also hasinfo, whicheun-button/eun-toggle-group's ownvariantdoesn't). - A required title left empty surfaces an inline
role="alert"message oncerequestSave()/the built-in Save button is attempted, not proactively while still typing, so a screen reader isn't interrupted before the user has even finished the field.