Skip to content

Commit 2200043

Browse files
serpentbladeclaude
andcommitted
feat(fullcalendar): consumer-extensible plugins (merged :options.plugins) + noEventsContent slot (10)
- merge baked-in PLUGINS with consumer-supplied :options.plugins instead of clobbering — list/rrule/premium/etc. engageable with zero bundle cost - pre-declare the noEventsContent portal-slot (9->10), inert until a consumer adds @fullcalendar/list via the merge - all-slots behavioral spec asserts slot-noEventsContent via a consumer-supplied list plugin (listWeek + zero events) — transitively proves the plugin-merge on all 6 targets - demo builds the :options object in <script> so Angular AOT retains the listPlugin import; VR host pins @fullcalendar/list to its ESM entry + excludes the @fullcalendar/* family from dep pre-bundling (CJS default interop yielded undefined -> buildPluginHooks crash) - @fullcalendar/list added as a VR-host devDependency only (NOT a wrapper dep) - docs: noEventsContent promoted to a Slots-table row + new Adding plugins section; comparison page 9->10 slots + merged-plugins differentiator Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dad80f5 commit 2200043

8 files changed

Lines changed: 253 additions & 37 deletions

File tree

docs/guide/fullcalendar-comparison.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ framework targets from a single source, each a pre-compiled
1313
`@rozie-ui/fullcalendar-*` package with no Rozie toolchain required. Where the
1414
ecosystem ships three official connectors plus scattered community coverage,
1515
Rozie ships the *same* 13-prop / 11-event surface, the *same* eight-verb
16-
imperative handle, the *same* nine custom-content portal slots, and the *same*
17-
`:options` long-tail passthrough on all six.
16+
imperative handle, the *same* ten custom-content portal slots, and the *same*
17+
`:options` long-tail passthrough — including consumer-engageable plugins beyond
18+
the bundled set — on all six.
1819

1920
Every wrapper on this page — including Rozie's — drives the **same
2021
`@fullcalendar/core` engine, current release `6.1.20`, published
@@ -32,7 +33,7 @@ evidence of absence.)
3233

3334
| Wrapper | Frameworks | Last published | Latest-framework support | Runtime option reconcile | Imperative handle | Custom event content |
3435
| ------- | ---------- | -------------- | ------------------------ | :----------------------: | :---------------: | :------------------: |
35-
| **[Rozie @rozie-ui/fullcalendar](/guide/fullcalendar)** | **6 — React + Vue + Svelte + Angular + Solid + Lit** | this repo (2026-06) | R18+ / V3.4+ / Sv5 / Ng19+ / Solid / Lit | ✓ managed `$watch``setOption` + `:options` long-tail | ✓ uniform `$expose` (8 verbs) |9 portal-slots, all 6 |
36+
| **[Rozie @rozie-ui/fullcalendar](/guide/fullcalendar)** | **6 — React + Vue + Svelte + Angular + Solid + Lit** | this repo (2026-06) | R18+ / V3.4+ / Sv5 / Ng19+ / Solid / Lit | ✓ managed `$watch``setOption` + `:options` long-tail (+ merged `:options.plugins`) | ✓ uniform `$expose` (8 verbs) |10 portal-slots, all 6 |
3637
| [@fullcalendar/react](https://www.npmjs.com/package/@fullcalendar/react) *(official)* | React | 6.1.20 · 2025-12 | **React ≤ 18** (peer `^16.7 \|\| ^17 \|\| ^18`) | ✓ diff → re-render |`ref``getApi()` |`eventContent` render-prop |
3738
| [@fullcalendar/vue3](https://www.npmjs.com/package/@fullcalendar/vue3) *(official)* | Vue 3 | 6.1.20 · 2025-12 | Vue 3.0.11+ (✓ 3.4+) | ✓ watches options |`ref``getApi()` | ✓ scoped slot |
3839
| [@fullcalendar/angular](https://www.npmjs.com/package/@fullcalendar/angular) *(official)* | Angular | 6.1.20 · 2025-12 | Angular **12 – 21** (✓ 19+) | ✓ diffs `[options]` |`getApi()` |`ng-template` |
@@ -84,26 +85,33 @@ registry, the GitHub API, and each project's README/`package.json` on
8485
- **The full `*Content` slot set as portal slots, everywhere.** FullCalendar's
8586
content hooks are exactly the kind of feature that fragments per framework —
8687
a render-prop in React, a slot in Vue, a template in Angular, and unsupported
87-
in the wrappers that don't reach those frameworks. Rozie surfaces **nine** of
88+
in the wrappers that don't reach those frameworks. Rozie surfaces **ten** of
8889
them — `event`, `dayCell`, `dayHeader`, `slotLabel`, `weekNumber`,
89-
`nowIndicatorContent`, `moreLink`, `allDayContent`, and `slotLaneContent` — as
90-
portal slots, each emitting the
90+
`nowIndicatorContent`, `moreLink`, `allDayContent`, `slotLaneContent`, and
91+
`noEventsContent` — as portal slots, each emitting the
9192
framework's idiomatic consumer surface (React / Solid render-prop, Vue
9293
scoped-slot, Svelte snippet, Angular content-child, Lit slot bridge),
9394
documented per-target in [Slots](/guide/fullcalendar#slots). Every slot is
9495
guarded — omit it and you get FullCalendar's default rendering on every target.
96+
(`noEventsContent` is pre-declared but inert until you engage the list plugin —
97+
see the merged-plugins note below.)
9598

9699
- **A `:options` long-tail passthrough, applied uniformly.** The curated
97100
13-prop surface stays primary, but FullCalendar exposes far more options and
98101
hooks than any wrapper can curate. Rozie's
99102
[`options` prop](/guide/fullcalendar#props) is an arbitrary passthrough bag —
100103
spread *first* into the engine config so curated props/events/slots win on key
101104
collision — that forwards anything the curated surface doesn't special-case
102-
(`businessHours`, `dayMaxEvents`, `*DidMount` hooks, object locales, the one
103-
excluded `noEventsContent` hook — list-view only, pending a future
104-
`@fullcalendar/list` plugin, …)
105+
(`businessHours`, `dayMaxEvents`, `*DidMount` hooks, object locales, …)
105106
identically on all six targets, with per-key `setOption` runtime reconcile.
106107
This closes the "but my app needs option X" gap without forking the wrapper.
108+
**Plugins beyond the bundled set are consumer-engageable the same way:**
109+
`:options.plugins` is the one curated key that *merges* (rather than clobbers)
110+
with the baked-in defaults (dayGrid/timeGrid/interaction) — import any
111+
FullCalendar plugin (`@fullcalendar/list` for list views + `noEventsContent`,
112+
`rrule`, `google-calendar`, premium scheduler plugins, …) and pass it through.
113+
One opt-in mechanism, all six targets, zero per-plugin wrapper code, zero
114+
bundle cost for plugins you don't engage.
107115

108116
- **Managed runtime reconcile, applied uniformly.** Each runtime-updatable prop
109117
`view` / `weekends` / `editable` / `selectable` / `height` / `nowIndicator`

docs/guide/fullcalendar.md

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
`FullCalendar` is Rozie's data-bound port of [FullCalendar](https://fullcalendar.io/) — the vanilla-JS calendar/scheduler engine. One `.rozie` source file ships idiomatic React, Vue, Svelte, Angular, Solid, and Lit consumers from a single wrapper. FullCalendar already publishes four official wrappers ([@fullcalendar/react](https://www.npmjs.com/package/@fullcalendar/react), [@fullcalendar/vue3](https://www.npmjs.com/package/@fullcalendar/vue3), [@fullcalendar/angular](https://www.npmjs.com/package/@fullcalendar/angular), [@fullcalendar/svelte](https://www.npmjs.com/package/@fullcalendar/svelte)) — each one wraps the same `Calendar` engine. Rozie collapses all of them (plus the Solid and Lit wrappers that **do not exist upstream**) into one source.
44

5-
This page is the **show-and-tell**: the API surface, per-framework quick starts, the imperative handle, the `:options` long-tail passthrough, and the per-target recipe for the nine custom-content portal slots.
5+
This page is the **show-and-tell**: the API surface, per-framework quick starts, the imperative handle, the `:options` long-tail passthrough, the opt-in plugin-extension model, and the per-target recipe for the ten custom-content portal slots.
66

77
The full source for `FullCalendar.rozie` lives in the [`@rozie-ui/fullcalendar` package](https://github.com/One-Learning-Community/rozie.js/blob/main/packages/ui/fullcalendar/src/FullCalendar.rozie).
88

@@ -205,7 +205,7 @@ The handle method names are clear of all eleven event names and thirteen prop na
205205

206206
## Slots
207207

208-
The wrapper surfaces **nine** of FullCalendar's `*Content` render hooks as portal slots — one authoring surface each that the per-target compiler routes through the framework's native imperative-render API (React/Solid render prop, Vue scoped slot, Svelte snippet, Angular content-child `<ng-template>`, Lit property bridge). Each slot is **guarded** in the wrapper: fill it and your fragment renders; leave it unfilled and FullCalendar's default rendering stands. Every slot receives one scope param, `arg` — FullCalendar's render argument for that hook.
208+
The wrapper surfaces **ten** of FullCalendar's `*Content` render hooks as portal slots — one authoring surface each that the per-target compiler routes through the framework's native imperative-render API (React/Solid render prop, Vue scoped slot, Svelte snippet, Angular content-child `<ng-template>`, Lit property bridge). Each slot is **guarded** in the wrapper: fill it and your fragment renders; leave it unfilled and FullCalendar's default rendering stands. Every slot receives one scope param, `arg` — FullCalendar's render argument for that hook.
209209

210210
| Slot | FullCalendar option | Renders | Demo-verified |
211211
| --- | --- | --- | :---: |
@@ -218,8 +218,9 @@ The wrapper surfaces **nine** of FullCalendar's `*Content` render hooks as porta
218218
| `moreLink` | `moreLinkContent` | "+N more" link content (`arg.num`, `arg.text`, …) | |
219219
| `allDayContent` | `allDayContent` | Time-grid all-day axis label content (`arg.text`, …) | |
220220
| `slotLaneContent` | `slotLaneContent` | Time-grid time-slot lane content (`arg.date`, `arg.time`, …) | |
221+
| `noEventsContent` | `noEventsContent` | List-view "no events to display" content (`arg.text`, …) — **inert** until you engage `@fullcalendar/list` via [`:options.plugins`](#adding-plugins) and show an empty list view ||
221222

222-
All nine share the **identical** per-target authoring shape shown below — the only thing that changes is the slot name and the `arg` payload (per FullCalendar's hook for that surface). The three demo-verified slots (`event`, `dayCell`, `dayHeader`) are wired into the VR matrix; the six long-tail slots (`slotLabel`, `weekNumber`, `nowIndicatorContent`, `moreLink`, `allDayContent`, `slotLaneContent`) use the same recipe with no extra ceremony.
223+
All ten share the **identical** per-target authoring shape shown below — the only thing that changes is the slot name and the `arg` payload (per FullCalendar's hook for that surface). The three pixel-baselined slots (`event`, `dayCell`, `dayHeader`) are wired into the VR matrix; the seven long-tail slots (`slotLabel`, `weekNumber`, `nowIndicatorContent`, `moreLink`, `allDayContent`, `slotLaneContent`, `noEventsContent`) use the same recipe with no extra ceremony. The all-slots behavioral spec mounts all ten across all six targets — including `noEventsContent` via a consumer-supplied list plugin, which transitively proves the [plugin merge](#adding-plugins).
223224

224225
> The current-time-indicator **slot** is named `nowIndicatorContent` (after FullCalendar's `nowIndicatorContent` option) so it does **not** clash with the boolean `nowIndicator` **prop** — a slot whose name equals a declared prop name is a hard compile error in Rozie (Svelte 5 unifies snippets and props into one `$props` namespace). Fill `#nowIndicatorContent` to customize the indicator content, and set the `nowIndicator` prop to `true` to actually enable it.
225226
@@ -363,7 +364,7 @@ The Solid (`dayHeader={…}`), Svelte (`{#snippet dayHeader(…)}`), Angular (`<
363364

364365
### The long-tail slots
365366

366-
`slotLabel`, `weekNumber`, `nowIndicatorContent`, `moreLink`, `allDayContent`, and `slotLaneContent` use the **same shared recipe** — fill the like-named slot and read its `arg`. Each maps one-to-one to a FullCalendar `*Content` option:
367+
`slotLabel`, `weekNumber`, `nowIndicatorContent`, `moreLink`, `allDayContent`, `slotLaneContent`, and `noEventsContent` use the **same shared recipe** — fill the like-named slot and read its `arg`. Each maps one-to-one to a FullCalendar `*Content` option:
367368

368369
| Slot | Option | Typical `arg` fields |
369370
| --- | --- | --- |
@@ -373,8 +374,9 @@ The Solid (`dayHeader={…}`), Svelte (`{#snippet dayHeader(…)}`), Angular (`<
373374
| `moreLink` | `moreLinkContent` | `arg.num`, `arg.text` |
374375
| `allDayContent` | `allDayContent` | `arg.text` |
375376
| `slotLaneContent` | `slotLaneContent` | `arg.date`, `arg.time` |
377+
| `noEventsContent` | `noEventsContent` | `arg.text` |
376378

377-
`allDayContent` and `slotLaneContent` render in time-grid views (`timeGridWeek` / `timeGridDay`) — the all-day axis label and the per-slot lane content respectively. Both work with the bundled `@fullcalendar/timegrid` plugin, no extra peer required.
379+
`allDayContent` and `slotLaneContent` render in time-grid views (`timeGridWeek` / `timeGridDay`) — the all-day axis label and the per-slot lane content respectively. Both work with the bundled `@fullcalendar/timegrid` plugin, no extra peer required. `noEventsContent` is **pre-declared but inert** with the bundled plugin set: it surfaces only in a list view (`listWeek` / `listDay` / `listMonth`) showing zero events, and list views exist only once you add the free `@fullcalendar/list` plugin — see [Adding plugins](#adding-plugins).
378380

379381
For example, a custom week-number badge in Vue:
380382

@@ -386,13 +388,44 @@ For example, a custom week-number badge in Vue:
386388
</FullCalendar>
387389
```
388390

389-
### Slots the wrapper does not surface
391+
### Adding plugins
390392

391-
One of FullCalendar's `*Content` hooks is **deliberately not** wrapped as a named slot (a documented exclusion, not a gap):
393+
The wrapper ships **batteries-included, zero-config**: three FullCalendar plugins are baked in and always on — `@fullcalendar/daygrid`, `@fullcalendar/timegrid`, and `@fullcalendar/interaction`. Those cover month/week/day grids plus drag-select-resize out of the box, so the common case needs no plugin wiring at all.
392394

393-
- **`noEventsContent`** — list-view only; `@fullcalendar/list` is not a bundled engine peer (pending a future list-plugin addition).
395+
When you need more, the wrapper is **consumer-extensible**: import any FullCalendar plugin and pass it through the `:options.plugins` passthrough. The wrapper **merges** your plugins with the baked-in defaults rather than replacing them (FullCalendar dedupes by identity, so re-passing a default is harmless). There is **no per-plugin wrapper code and no bundle cost** for plugins you don't engage — the extra plugin lives in *your* dependency tree, not the wrapper's.
394396

395-
Need it? Reach for the **`:options` passthrough** (pass `{ noEventsContent: … }` straight through) or grab the raw engine via `getApi()` and set the option imperatively.
397+
The worked example is the list view + its `noEventsContent` slot. Add the free `@fullcalendar/list` plugin, pass it via `:options.plugins`, switch to a `listWeek`/`listDay`/`listMonth` view, and the `noEventsContent` portal slot becomes live (it renders when the list is empty):
398+
399+
```bash
400+
npm i @fullcalendar/list
401+
```
402+
403+
```vue
404+
<script setup lang="ts">
405+
import { ref } from 'vue';
406+
import FullCalendar from '@rozie-ui/fullcalendar-vue';
407+
import listPlugin from '@fullcalendar/list';
408+
409+
const view = ref('listWeek');
410+
const events = ref([]); // empty → noEventsContent fires
411+
</script>
412+
413+
<template>
414+
<FullCalendar
415+
v-model:view="view"
416+
:events="events"
417+
:options="{ plugins: [listPlugin] }"
418+
>
419+
<template #noEventsContent="{ arg }">
420+
<span class="empty">Nothing scheduled — enjoy your day.</span>
421+
</template>
422+
</FullCalendar>
423+
</template>
424+
```
425+
426+
The same opt-in mechanism engages every other FullCalendar plugin uniformly across all six targets — **`@fullcalendar/rrule`** (recurring events; recurring fields like `rrule` already pass through `:events` untouched), **`@fullcalendar/google-calendar`**, **`@fullcalendar/luxon3`** / time-zone plugins, and the bootstrap/theme plugins. The **premium** scheduler plugins (`@fullcalendar/resource-timeline`, `@fullcalendar/resource-timegrid`, …) work the same way — pass the plugin via `:options.plugins` and additionally supply your `schedulerLicenseKey` through `:options` (`:options="{ plugins: [resourceTimelinePlugin], schedulerLicenseKey: '…' }"`).
427+
428+
If you'd rather drive a one-off option imperatively, the `:options` passthrough also forwards arbitrary non-plugin options, and `getApi()` returns the raw `Calendar` instance for full control.
396429

397430
## Recipes
398431

0 commit comments

Comments
 (0)