Packages

Dates for Phoenix: date, range, time and recurrence pickers for LiveView forms, with Ecto types and the same rules on the server.

Current section

Files

Jump to
koyomi README.md
Raw

README.md

# Koyomi

![Koyomi — Dates for Phoenix](https://raw.githubusercontent.com/hattoriai/koyomi/main/media/banner.png)

Koyomi (暦, "calendar") is dates for Phoenix. It gives your app:

* pickers for LiveView forms: a day, a range of days, several days, a
  month, an ISO week, a year, a time, a day and a time (in a time zone,
  or from the time slots your LiveView offers), and a repeat rule;
* fields of segments in the order of the locale (`15/03/2027`,
  `03/15/2027`, `2027/03/15`), typed or changed with the arrow keys, and
  dates typed in words, with a preview: "next friday", "in 3 weeks",
  "15–20 mar", "amanhã às 9h";
* calendars in any locale and in the calendar systems of the browser:
  Japanese, Buddhist, Persian, Islamic, Hebrew, Chinese and more, with the
  locale's digits, first day of the week and direction;
* `Koyomi.Rules`, the same rules in the browser and in your changeset, so
  that what the calendar disables is what the server refuses;
* Ecto types for what the pickers post, PostgreSQL `daterange` and
  `tstzrange` included;
* day content from your LiveView: prices, full days, marks and time
  slots, as for a booking;
* `Koyomi.Test`, to pick values in `Phoenix.LiveViewTest` with no
  browser.

The pickers are a prebuilt JavaScript bundle in the Hex package. Your app
does not run Node.js for them: you add the hook and one style sheet.

## What the pickers do

| | |
| --- | --- |
| Pickers | `date_picker`, `date_range_picker`, `dates_picker`, `month_picker`, `week_picker`, `year_picker`, `time_picker`, `datetime_picker`, `recurrence_picker`, and `calendar` for a calendar on the page |
| Fields | segments in the locale's order and digits, typed or changed with the arrow keys, the number keyboard on a phone, two-digit years, paste |
| Words | dates, ranges and times typed in words, in English, Portuguese, Spanish, French, German, Italian, Dutch and Japanese, and month and weekday names in every locale, with a preview |
| Calendar | one or more months side by side, a list of months that scrolls on a phone, months and years to jump to, week numbers, holidays of 26 countries, presets |
| Ranges | drawn as the pointer moves, the length in days or nights, days out of reach disabled while the end is chosen |
| Rules | bounds relative to today, disabled days, ranges and weekdays, the shortest and longest range, counts, times and steps; the same in `Koyomi.Changeset` |
| Locales | names, digits, first day of the week and weekend from the browser's CLDR data, right-to-left languages, the 12 or 24-hour clock |
| Calendars | Gregorian, ISO, Japanese, Buddhist, ROC, Persian, Islamic (four kinds), Hebrew, Chinese, Dangi, Indian, Ethiopic, Coptic |
| Time zones | today in a zone, wall clock times posted with the zone's offset, the hour the clocks skip or repeat shown and resolved |
| Day content | labels, tones, marks, descriptions, disabled days and time slots from the LiveView, in sync or async, refreshed on demand |
| Forms | the forms of the browser's own inputs (`2027-03-15`, `2027-W11`), the browser's own input before the hook mounts, `phx-change` and `used_input?`, pages without LiveView |
| Types | `Koyomi.DateRange`, `Koyomi.DateTimeRange`, `Koyomi.Dates`, `Koyomi.Month`, `Koyomi.Week`, `Koyomi.Recurrence` |
| Repeat | daily, weekly, monthly and yearly rules (RFC 5545), said in words in eight languages, with the next dates |
| Theming | CSS custom properties, the Sumi theme, dark mode, forced colours, reduced motion |
| Accessibility | a labelled group of spinbuttons, a dialog, the WAI-ARIA grid with one tab stop, announcements, 44px targets on touch screens |

## Install

Koyomi needs Elixir 1.18 or later (it uses the built-in `JSON` module),
Phoenix 1.8 and Phoenix LiveView 1.2.

Add `koyomi` to the deps in `mix.exs`:

```elixir
def deps do
  [
    {:koyomi, "~> 0.1"}
  ]
end
```

Then run:

```sh
mix deps.get
mix koyomi.install
```

The installer adds the hook to `assets/js/app.js` and the style sheet to
`assets/css/app.css`, and prints what it changed. Give `--dry-run` to see
the changes first. It does not add what a file already has, so you can
run it again.

### The manual steps

To make the same changes by hand, import the hook in `assets/js/app.js`
and add it to the hooks of the `LiveSocket`:

```js
import { Koyomi } from "koyomi"

const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {...colocatedHooks, Koyomi},
})
```

Import the style sheet in `assets/css/app.css` (and the Sumi theme, if you
want it: Sumi is the design system of Hattori AI, the makers of Koyomi
and Kotoba). Put these lines below the other `@import` lines of
`app.css`:

```css
@import "../../deps/koyomi/priv/static/koyomi.css";
/* @import "../../deps/koyomi/priv/static/koyomi-sumi.css"; */
```

A date and time picker with a `time_zone` other than UTC, and rules with a
`:time_zone`, need a time zone database on the server, such as
[`tz`](https://hex.pm/packages/tz) or
[`tzdata`](https://hex.pm/packages/tzdata):

```elixir
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
```

### `NODE_PATH=deps`

`import { Koyomi } from "koyomi"` resolves through the `NODE_PATH` of your
esbuild profile, which must have `deps/`. The Phoenix generators already
set it. If your profile has no `NODE_PATH`, add it in `config/config.exs`:

```elixir
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
```

## Use

```elixir
schema "bookings" do
  field :check_in, :date
  field :stay, Koyomi.DateRange
end

@stay_rules Koyomi.Rules.new(min: :today, max_days: 14)

def changeset(booking, attrs) do
  booking
  |> cast(attrs, [:check_in, :stay])
  |> Koyomi.Changeset.validate_range(:stay, @stay_rules)
end
```

```heex
<.form for={@form} phx-change="validate" phx-submit="save">
  <label id="stay-label">Stay</label>
  <.date_range_picker field={@form[:stay]} label_id="stay-label" rules={@stay_rules} months={2} nights />
</.form>
```

Import the components in the `html_helpers` of your web module:

```elixir
import Koyomi.Components
```

Each picker posts its value with the form, in the form of the browser's
own inputs (`2027-03-15`, or `stay[from]` and `stay[to]` for a range), so
the form's own `phx-change` and `phx-submit` events have it, and the Ecto
type of the field casts it.

## Guides

* [Quickstart](guides/quickstart.md): install, the schema, the form and
  the changeset.
* [Pickers](guides/pickers.md): every picker, what it posts and its
  attributes.
* [Forms](guides/forms.md): what the pickers post, errors, pages without
  LiveView, values set by the server.
* [Rules](guides/rules.md): `Koyomi.Rules` and `Koyomi.Changeset`.
* [Typing](guides/typing.md): the segments, and dates typed in words.
* [Calendars and locales](guides/calendars.md): locales, calendar systems,
  week numbers and holidays.
* [Time zones](guides/time_zones.md): today in a zone, offsets, the hours
  the clocks skip or repeat.
* [Day content](guides/days.md): labels, marks, disabled days and time
  slots from the LiveView.
* [Recurrence](guides/recurrence.md): the repeat picker and
  `Koyomi.Recurrence`.
* [Testing](guides/testing.md): `Koyomi.Test` with `Phoenix.LiveViewTest`.
* [Theming](guides/theming.md): the `--koyomi-*` properties and the Sumi
  theme.
* [Accessibility](guides/accessibility.md): the roles and the keyboard.
* [Known limits](guides/limits.md).

## Development

The picker source is TypeScript in `assets/`. The built files in
`priv/static` are not in the repository; the Hex package has them.

```sh
mix deps.get
mix koyomi.build    # npm ci in assets/ when necessary, then esbuild
mix dev             # the development server on http://localhost:4099
```

`mix koyomi.build` runs in the `:dev` environment and needs `npm`. It
empties `priv/static` and writes the four files of the package:
`koyomi.esm.js`, `koyomi.cjs.js`, `koyomi.css` and `koyomi-sumi.css`.

`mix dev` runs `dev.exs`: a Phoenix endpoint with every picker in a form
(`/`), day content from a fake availability service, slow and async
(`/days`), the calendar systems of the world (`/calendars`), a page
without LiveView (`/plain`), and the Sumi theme at `/?theme=sumi` (and
`&dark=1`). Every page takes `locale`, `calendar` and `zone` query
params. Set `PORT` for another port. An esbuild watcher rebuilds the
bundle, and the page reloads.

### Tests

```sh
mix test                   # the ExUnit suite
mix test --include build   # also mix koyomi.build and mix hex.build
mix test.js                # the unit tests of the TypeScript modules
mix test.e2e               # the Playwright browser tests
mix precommit              # compile, format, credo --strict and test
mix dialyzer
```

`mix test.js` runs the tests of the pure modules in `assets/test/` (the
date arithmetic, the locales and calendars, the words parser and the
recurrence rules) with Node's test runner. `test/fixtures/recurrence.json`
holds the recurrence rules to the same results in Elixir and in
TypeScript.

The browser tests in `e2e/` use Playwright, in Chromium, Firefox and
WebKit (the engine of Safari). Install the browsers once:

```sh
cd e2e && npm ci && npx playwright install chromium firefox webkit
```

`mix test.e2e` runs every spec in the three browsers, and `E2E_BROWSERS`
picks some of them. It runs `npm ci` in `e2e/` when `e2e/node_modules`
is missing, builds the bundle, and starts its own development server,
without watchers, on port 4098 (set `E2E_PORT` for another port). A
`mix dev` on port 4099 can keep running.

### Release

`mix release` (in this repository, an alias that replaces Mix's `release`
task) builds the bundle, checks that `priv/static` holds only the four
bundle files (`mix koyomi.release_check`), publishes to Hex, and tags
and pushes `v<version>`.

## License

Koyomi is released under the MIT License. See `LICENSE`.

The table of week data by region follows the Unicode CLDR (Unicode
License v3). See `NOTICE`.