It is not necessary to use a framework with Cally. However, by virtue of being written as web components, Cally is framework-agnostic and can be used anywhere.
Most frameworks support web components out of the box, requiring little-to-no setup. Here we will walk through how to use Cally in React, Vue, and Svelte. The process should be similar for other frameworks.
React
React does not have great support for web components. It is advised to create React-specific wrappers around web components so that they can be used easily and idiomatically.
Note: the next major version of React will offer full support for web components, making these wrappers unnecessary. Though no release date has been set yet.
With a couple of custom hooks and some thin wrappers, we can make Cally's components feel "native" to React.
import { useEffect, useRef, forwardRef, useImperativeHandle } from "react";
import "cally";
function useListener(ref, event, listener) {
useEffect(() => {
const current = ref.current;
if (current && listener) {
current.addEventListener(event, listener);
return () => current.removeEventListener(event, listener);
}
}, [ref, event, listener]);
}
function useProperty(ref, prop, value) {
useEffect(() => {
if (ref.current) {
ref.current[prop] = value;
}
}, [ref, prop, value]);
}
export const CalendarMonth = forwardRef(function CalendarMonth(props, forwardedRef) {
return <calendar-month offset={props.offset} ref={forwardedRef} />;
});
export const CalendarRange = forwardRef(function CalendarRange(
{ onChange, showOutsideDays, firstDayOfWeek, isDateDisallowed, ...props },
forwardedRef
) {
const ref = useRef();
useImperativeHandle(forwardedRef, () => ref.current, []);
useListener(ref, "change", onChange);
useProperty(ref, "isDateDisallowed", isDateDisallowed);
return (
<calendar-range
ref={ref}
show-outside-days={showOutsideDays || undefined}
first-day-of-week={firstDayOfWeek}
{...props}
/>
);
});
export const CalendarDate = forwardRef(function CalendarDate(
{ onChange, showOutsideDays, firstDayOfWeek, isDateDisallowed, ...props },
forwardedRef
) {
const ref = useRef();
useImperativeHandle(forwardedRef, () => ref.current, []);
useListener(ref, "change", onChange);
useProperty(ref, "isDateDisallowed", isDateDisallowed);
return (
<calendar-date
ref={ref}
show-outside-days={showOutsideDays || undefined}
first-day-of-week={firstDayOfWeek}
{...props}
/>
);
});
Usage
You can now use these components as you would any other React component.
import { useState } from "react";
import { createRoot } from 'react-dom/client';
import { CalendarRange, CalendarMonth } from "./Cally";
function Picker({ value, onChange }) {
return (
<CalendarRange value={value} onChange={onChange}>
<CalendarMonth />
<CalendarMonth offset={1} />
</CalendarRange>
);
}
function App() {
const [value, setValue] = useState("");
const onChange = (event) => setValue(event.target.value);
return (
<>
<p>Value is: {value}</p>
<Picker value={value} onChange={onChange} />
</>
)
}
const root = createRoot(document.getElementById("root"));
root.render(<App />)
TypeScript
Cally exports types for each component's props. We can use these types to add type-checking and improve the editor experience.
import {
useEffect,
useRef,
ReactNode,
forwardRef,
useImperativeHandle,
type RefObject,
type PropsWithChildren,
} from "react";
import "cally";
import type {
CalendarRangeProps,
CalendarMonthProps,
CalendarDateProps,
} from "cally";
declare global {
namespace JSX {
interface IntrinsicElements {
"calendar-month": unknown;
"calendar-range": unknown;
"calendar-date": unknown;
}
}
}
function useListener(
ref: RefObject<HTMLElement>,
event: string,
listener?: (e: Event) => void
) {
useEffect(() => {
const current = ref.current;
if (current && listener) {
current.addEventListener(event, listener);
return () => current.removeEventListener(event, listener);
}
}, [ref, event, listener]);
}
function useProperty(ref: RefObject<HTMLElement>, prop: string, value?: any) {
useEffect(() => {
if (ref.current) {
// @ts-expect-error - TS doesn't know that `prop` is a key
ref.current[prop] = value;
}
}, [ref, prop, value]);
}
export const CalendarMonth = forwardRef(function CalendarMonth(
props: CalendarMonthProps,
forwardedRef
) {
return <calendar-month offset={props.offset} ref={forwardedRef} />;
});
export const CalendarRange = forwardRef(function CalendarRange(
{
onChange,
showOutsideDays,
firstDayOfWeek,
isDateDisallowed,
...props
}: PropsWithChildren<CalendarRangeProps>,
forwardedRef
) {
const ref = useRef<HTMLElement>(null);
useImperativeHandle(forwardedRef, () => ref.current, []);
useListener(ref, "change", onChange);
useProperty(ref, "isDateDisallowed", isDateDisallowed);
return (
<calendar-range
ref={ref}
show-outside-days={showOutsideDays || undefined}
first-day-of-week={firstDayOfWeek}
{...props}
/>
);
});
export const CalendarDate = forwardRef(function CalendarDate(
{
onChange,
showOutsideDays,
firstDayOfWeek,
isDateDisallowed,
...props
}: PropsWithChildren<CalendarDateProps>,
forwardedRef
) {
const ref = useRef<HTMLElement>(null);
useImperativeHandle(forwardedRef, () => ref.current, []);
useListener(ref, "change", onChange);
useProperty(ref, "isDateDisallowed", isDateDisallowed);
return (
<calendar-date
ref={forwardRef}
show-outside-days={showOutsideDays ? "" : undefined}
first-day-of-week={firstDayOfWeek}
{...props}
/>
);
});
Vue
Vue has excellent support for web components. If you haven't already, you need to configure vue to understand web components. After that, they can be used directly.
<script setup>
import 'cally';
</script>
<template>
<calendar-range :months="2">
<calendar-month />
<calendar-month :offset="1" />
</calendar-range>
</template>
Usage with v-model
The <calendar-date>
and <calendar-range>
components
emit change
events when their value changes. You can use the
v-model
directive to bind ref
s to these events.
As noted in the Vue docs, v-model
listens for
input
events by default. But by using the
.lazy
modifier, it will listen for change
events.
<script setup>
import 'cally';
const selected = ref("")
</script>
<template>
<p>Selected range: {{ selected }}</p>
<calendar-range :months="2" v-model.lazy="selected">
<calendar-month />
<calendar-month :offset="1" />
</calendar-range>
</template>
You are not required to use v-model
. You can listen for events
yourself if you prefer:
<script setup>
import 'cally';
const selected = ref("")
function onChange(event) {
selected.value = event.target.value
}
</script>
<template>
<p>Selected range: {{ selected }}</p>
<calendar-range :months="2" :value="selected" @change="onChange">
<calendar-month />
<calendar-month :offset="1" />
</calendar-range>
</template>
TypeScript
If you are using TypeScript, you can augment Vue's types to add type-checking and improve your editor experience. Cally exports types for each component's props making this a simple, one-time procedure.
First you should create a d.ts
file in your Vue project, with any
name you like. For this example let's call it globals.d.ts
. In
that file, paste the following code:
import type { DefineComponent } from "vue";
import type {
CalendarRangeProps,
CalendarMonthProps,
CalendarDateProps,
} from "cally";
interface CallyComponents {
"calendar-range": DefineComponent<CalendarRangeProps>;
"calendar-date": DefineComponent<CalendarDateProps>;
"calendar-month": DefineComponent<CalendarMonthProps>;
}
declare module "vue" {
interface GlobalComponents extends CallyComponents {}
}
declare global {
namespace JSX {
interface IntrinsicElements extends CallyComponents {}
}
}
This uses TypeScript's declaration merging feature. You can read more about it in the TypeScript Handbook.
Finally, you must add this to the compilerOptions.types
field in
your
tsconfig.json
:
{
// ...
"compilerOptions": {
// ...
"types": ["./globals.d.ts"]
}
}
Now you will get type-checking for both props and events, along with auto-complete in your editor.
Svelte
Svelte has good support for web components. There is no setup required to start using Web Components.
<script lang="ts">
import "cally";
</script>
<calendar-range months={2}>
<calendar-month></calendar-month>
<calendar-month offset={1}></calendar-month>
</calendar-range>
TypeScript
If you are using TypeScript, you can augment Svelte's types to add type-checking and improve your editor experience. Cally exports types for each component's props making this a simple, one-time procedure.
First you should create a d.ts
file in your Svelte project, with
any name you like. For this example let's call it globals.d.ts
.
In that file, paste the following code:
import type {
CalendarRangeProps,
CalendarMonthProps,
CalendarDateProps,
} from "cally";
type MapEvents<T> = {
[K in keyof T as K extends `on${infer E}` ? `on:${Lowercase<E>}` : K]: T[K];
};
declare module "svelte/elements" {
interface SvelteHTMLElements {
"calendar-range": MapEvents<CalendarRangeProps>;
"calendar-month": MapEvents<CalendarMonthProps>;
"calendar-date": MapEvents<CalendarDateProps>;
}
}
This uses TypeScript's declaration merging feature. You can read more about it in the TypeScript Handbook.
Finally, you must add this to the compilerOptions.types
field in
your
tsconfig.json
:
{
// ...
"compilerOptions": {
// ...
"types": ["./globals.d.ts"]
}
}
Now you will get type-checking for both props and events, along with auto-complete in your editor.