Skip to content

Calendar Recurrence

daywatch preview

April 2026

Mon
Tue
Wed
Thu
Fri
Sat
Sun
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
active day
excluded
outside bounds
Live config

Calendar recurrence

14 fields / useUmpire()
Date bounds
Window + clamp
Fixed Betweencondition not met
Explicit dates
Authoritative picks

Adding a date here activates the disables() rule — all pattern fields go dark and play() will recommend clearing any active values.

Explicit Dates
No explicit dates.
Patterns
Weekdays, months, month-days
Every Weekday
Every Month

oneOf active — weekday strategy is locked in. Day-of-month is unavailable until weekdays are cleared.

Every Date
No day picks.
conflicts with byWeekday strategy
Exclusions
Carve-outs from patterns
Except Dates
Except Between

A recurring event scheduler where fields interact in multiple ways at once. This is the kind of config panel that turns into a mess of useEffect chains without a system to manage it.

A calendar app lets users define how events repeat. The options are interdependent — and none of this is a “form” in the traditional sense:

  • Explicit dates override all pattern-based recurrence (if you pick specific dates, weekday/monthly patterns don’t apply)
  • Two scheduling strategies are mutually exclusive: pick specific hours, or define a start/end time interval — not both
  • Interval fields depend on each other: you can’t set a repeat interval or end time without a start time
  • Exclusions only make sense when a recurrence pattern exists
  • “Fixed between” bounds only make sense when both a start and end date are set
import { disables, enabledWhen, oneOf, requires, umpire } from '@umpire/core'
const scheduler = umpire({
fields: {
// Date bounds
fromDate: {},
toDate: {},
fixedBetween: { default: false },
// Explicit dates (overrides patterns)
dates: {},
// Day-level recurrence patterns
everyWeekday: {},
everyDate: {},
everyMonth: {},
// Exclusions from patterns
exceptDates: {},
exceptBetween: {},
// Sub-day strategy A: specific hours
everyHour: {},
// Sub-day strategy B: time interval
startTime: {},
endTime: {},
repeatEvery: {},
// Shared
duration: {},
},
rules: [
// Explicit dates shut down everything pattern-based
disables('dates', [
'everyWeekday', 'everyDate', 'everyMonth',
'everyHour', 'startTime', 'endTime', 'repeatEvery',
'exceptDates', 'exceptBetween',
]),
// Pick one: specific hours OR a time interval
oneOf('subDayStrategy', {
hourList: ['everyHour'],
interval: ['startTime', 'endTime', 'repeatEvery'],
}),
// Interval fields chain off startTime
requires('repeatEvery', 'startTime'),
requires('endTime', 'startTime'),
// Bounds toggle only meaningful when both dates exist
enabledWhen('fixedBetween',
({ fromDate, toDate }) => !!fromDate && !!toDate),
// Exclusions only meaningful when patterns exist
enabledWhen('exceptDates',
(v) => !!(v.everyWeekday || v.everyDate || v.everyMonth)),
enabledWhen('exceptBetween',
(v) => !!(v.everyWeekday || v.everyDate || v.everyMonth)),
],
})

Seven rules. Three different rule types working together. The declarations read as plain English.

let values = scheduler.init()
values = { ...values, dates: ['2026-04-01', '2026-04-05'] }
const result = scheduler.check(values)

Everything pattern-based shuts down:

Fieldenabledreason
everyWeekdayfalse'overridden by dates'
everyHourfalse'overridden by dates'
startTimefalse'overridden by dates'
exceptDatesfalse'overridden by dates'

One disables rule knocked out 9 fields. The reason string makes it obvious why.

User clears explicit dates and sets up a Monday/Wednesday/Friday pattern:

const prev = values
values = { ...values, dates: undefined, everyWeekday: [1, 3, 5] }
const result = scheduler.check(values)

The field opens back up:

  • All pattern fields are available again
  • Both sub-day strategies are available (no branch chosen yet)
  • exceptDates and exceptBetween are now enabled — a recurrence pattern exists to exclude from

User sets a start time. This activates the interval branch of oneOf:

const prev2 = values
values = { ...values, startTime: '09:00' }
const result = scheduler.check(values, undefined, prev2)
Fieldenabledreason
everyHourfalse'conflicts with interval strategy'
startTimetruenull
endTimetruenull
repeatEverytruenull

Three things happened from one value change:

  1. oneOf detected that the interval branch is active (startTime has a value), disabling everyHour
  2. requires('endTime', 'startTime') is now satisfied — endTime becomes available
  3. requires('repeatEvery', 'startTime') is also satisfied

The prev parameter lets oneOf resolve ambiguity — it knows the interval branch is the newly active one.

If everyHour was empty, there’s nothing to clean up:

scheduler.play({ values: prev2 }, { values })
// → []

But if the user had previously set specific hours before switching strategies:

scheduler.play(
{ values: { ...prev2, everyHour: [9, 17] } },
{ values },
)
// → [{ field: 'everyHour',
// reason: 'conflicts with interval strategy',
// suggestedValue: undefined }]

play() only recommends clearing fields that (1) just became disabled, (2) hold a non-empty value, and (3) don’t already match their default. No false positives.

Three interaction patterns layered on top of each other:

  • disables — explicit dates are authoritative. A stale value in dates still shuts everything down, because Umpire checks the value, not the field’s availability. The consumer has to clear dates to release the pattern fields.
  • oneOf — the two scheduling strategies are mutually exclusive. No if/else chain needed — declare the branches and Umpire handles the rest.
  • requires — availability flows through the dependency chain. startTimeendTime and startTimerepeatEvery propagate naturally.

Seven declarative rules replace what would otherwise be a tangled web of useEffect hooks and conditional renders. Same idea works for any config panel, booking widget, or scheduler — not just forms.