Skip to content

Collections & groups

As a calendar grows, keeping every event in one flat list gets hard to reason about. The pattern we recommend for anything non-trivial: split events into named groups and compose the calendar from them. A group is an independent, toggleable, separately-exportable set — so you build from parts and maintain each part on its own.

You get a group two ways:

  • cal.addGroup({ id, events }) — declare one inline.
  • cal.load(collection) — load a portable collection (the plain-JSON .cdy the builder or the Compose tool produce). Load several to compose one calendar from many.

Both merge into the same calendar; every computed event carries the groupId it came from.

import { calendary } from "calendaryjs";
import { every, date } from "calendaryjs/builder";
const cal = calendary();
cal.addGroup({
id: "holidays",
events: [every("year").on(date(12, 25)).title("Winter break")],
});
cal.addGroup({
id: "team",
events: [every("week").on("monday").title("Standup")],
});
cal.getEventsInRange("2025-01-01", "2025-12-31"); // both groups, merged + ordered

A collection is a plain object (or JSON string) — { collection, plugins?, events } — the exact shape toCollection() and Compose emit. load() first checks that every plugin the collection declares is registered (one clear error listing any that aren’t), then adds the events as a group whose id is the collection’s name:

import { calendary } from "calendaryjs";
import { lunar } from "calendaryjs-plugin-lunar";
const publicHolidays = {
collection: "public-holidays",
plugins: ["calendaryjs-plugin-lunar"],
events: [
/* … plain event configs … */
],
};
const cal = calendary().use(lunar()); // install the plugins the collection declares
cal.load(publicHolidays); // → group "public-holidays"

load() never fetches or installs anything — you register plugins with use(), the collection only declares which it needs.

Section titled “Compose from multiple collections — the recommended shape”

Author one collection per concern, then load() each into its own group. load() returns the instance, so it chains:

const cal = calendary()
.use(lunar())
.load(publicHolidays) // → group "public-holidays"
.load(companyCalendar) // → group "company"
.load(personalCalendar); // → group "personal"
cal.getDay("2025-12-25").events; // every group's events for the day, merged + ordered

Give each collection a distinct collection name — that name becomes its group id, so distinct names keep the groups from colliding.

Why this beats one flat list:

  • Maintainable — edit public-holidays without touching company or personal.
  • Reusable — ship a collection as a .cdy file or an npm package; anyone load()s it. No event code to copy.
  • Toggleable — show or hide a whole collection at runtime (below).
  • Attributable — every event knows its groupId, so you can colour, filter, or export by collection.

One file, several collections — a bundle

Section titled “One file, several collections — a bundle”

You can also ship them as a single file. A .cdy may carry several named collections in a { collections: [...] } bundle; load() expands the whole bundle in one call, each collection keeping its own id / priority / color:

const bundle = {
collections: [
{ id: "public-holidays", priority: 100, events: [...] },
{ id: "personal", priority: 50, color: "teal", events: [...] },
],
};
calendary().load(bundle); // → groups "public-holidays" and "personal"

A single Collection and a bundle both flow through the same load() — pass whichever you have. Produce a bundle from a live instance with toBundle().

cal.getGroup("company"); // inspect one group
cal.setGroupEnabled("personal", false); // hide a whole collection — its events leave the output
cal.setGroupEnabled("personal", true); // show it again
cal.removeGroup("personal"); // drop it entirely

Because every event carries its origin, you can also filter in queries:

cal.search().source("public-holidays").year(2025).getEvents(); // one collection's events

toCollection() is the inverse of load() — it produces a portable collection (pass it to JSON.stringify for a .cdy). Export one group or the whole calendar:

cal.toCollection({ group: "company", name: "company" }); // just this collection
cal.toCollection({ name: "everything" }); // every group's events, flattened

To keep every group separate in one file — id / priority / color preserved — export a bundle instead of flattening:

cal.toBundle({ version: "1.0.0" }); // { collections: [ …one per group… ] }

No instance handy? buildCollection(configs, { name, plugins, schema }) assembles a .cdy from plain configs statelessly — it derives the plugin manifest from the plugins you pass and can stamp $schema:

import { buildCollection } from "calendaryjs";
const cdy = buildCollection(configs, {
name: "family",
plugins: [lunar()],
schema: "https://calendaryjs.dev/schema/cdy.json",
});

This is the question that matters most once you compose from several sources: what happens when one day has five events, each from a different collection?

calendaryjs keeps all of them — it never silently picks a winner or hides one. Every enabled group’s events for a day are merged into one list and ordered by priority (higher first, exactly like CSS z-index; default 0; ties keep generation order). The engine is a mechanism, not a policy: you decide precedence.

const cal = calendary()
.load(publicHolidays) // its events carry priority: 100
.load(company) // priority: 50
.load(personal); // priority: 0 (default)
const day = cal.getDay("2025-12-25");
day.events.length; // 5 — nothing dropped
day.events[0].title; // the highest-priority event = the day's "primary"

You have three levers, none of which delete anything:

  1. Order across collections. Author a collection’s events with a higher priority tier so that collection sits on top on busy days — see Precedence. Tag events with a source to filter or reason about them in bulk.
  2. Hide a whole collection. cal.setGroupEnabled(id, false) takes its events out of the output entirely — like un-checking a calendar in Google or Apple Calendar.
  3. Let one date yield. A single event can pre-declare that it steps aside on a known-busy date — drop or reschedule that date only — via onConflict, exceptions, or overrideDates. These are per-event and author-declared; they act on the one event, not on whatever else shares the day. See customizing a single occurrence.

So precedence between collections is just priority; visibility is setGroupEnabled; and a single event yielding on a specific date is onConflict / exceptions. Nothing is ever dropped without you asking.

Write a collection by hand as JSON, generate it from the builder with toCollection(), or compose one visually in Compose and export the .cdy. However you make it, load() reads it back the same way.