Cally

Integrating with frameworks

How to use with React/Vue/Svelte

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 refs 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.