Two real applications

Domma CMS holds the data. Domma JS carries the requests. domma-reactive keeps the page in step.

Counters prove syntax. These prove the point.

Both examples below are complete working applications on ordinary content pages of this site. They read and write Domma CMS collections through the public collection API, and nothing behind them was written by hand: no endpoint, no controller, no serialiser. A collection schema with three lines of api config is the whole back end.

Room booking

The Long Table, one of the six bookable rooms

Pick a room, pick a day, and see which hours are already gone. Choose a free hour, fill in a form that validates itself, and the booking is written to the bookings collection - where the CMS validates it, checks the room really exists, and stores it.

Shows: an effect that fetches, computed availability, derived validation, keyed slot grids, and a read allow-list that keeps other people's names off the page.

Open the booking demo

House hunter

A stone terrace on Mayfield Grove, Harrogate

Eight filters, one query. Every control on the form contributes a clause to the CMS filter DSL, the server answers it, and the results render as components with private state - expand one card and the others stay as they were.

Shows: a computed query, rateLimit on the search box, components with params by reference, a custom binding, a shortlist that persists itself, and a write to a create-only collection.

Open the house hunter

How both of them are built

The same four steps, and only the last one is JavaScript.

Rooms, bookings, properties and viewings are collections in Admin → Collections. Each has a schema: field names, types, what is required, and - for bookings and viewings - a reference field pointing at another collection, which the CMS validates on every write.

{
  "slug": "bookings",
  "fields": [
    {"name": "roomId", "label": "Room", "type": "reference", "required": true,
     "reference": {"collection": "rooms", "displayField": "name"}},
    {"name": "date", "label": "Date", "type": "date", "required": true},
    {"name": "startHour", "label": "Start hour", "type": "number", "required": true}
  ]
}

The api block on the schema is the entire back end. Each verb is separately switchable, and read.fields is an allow-list of what a public read may return.

{
  "api": {
    "read":   {"enabled": true,  "access": "public",
               "fields": ["roomId", "date", "startHour", "hours", "people", "status"]},
    "create": {"enabled": true,  "access": "public"},
    "update": {"enabled": false, "access": "admin"},
    "delete": {"enabled": false, "access": "admin"}
  }
}

The photographs are fields too. rooms and properties each carry an image holding a path under /media, and an imageCredit beside it - so a room's picture arrives in the same JSON as its price, and a page that wanted no pictures would simply not bind them. Nothing in either demo fetches an image; both bind a string the CMS handed them.

That publishes GET and POST /api/v1/bookings. name, email and notes are stored on every booking and are not in the read allow-list, so the public API cannot return them however the request is phrased - which is why the demo can show you that an hour is taken without showing you who took it.

Access can also be a role name (checked against a JWT) or token, for a project-scoped API token. viewings in the second demo publishes create and nothing else: the page can leave a request and cannot read anyone else's back.

The markup is ordinary HTML in an ordinary CMS page. data-* attributes survive the CMS sanitiser untouched, so a page author can write bindings without a plugin, a partial or a build.

<ol class="slots" data-each="slots key=hour">
    <li>
        <button type="button" class="slot"
                data-on-click="$parent.choose($data)"
                data-bind-disabled="taken"
                data-bind-class="taken ? 'is-taken' : ''">
            <span data-bind-text="label"></span>
        </button>
    </li>
</ol>

Each demo is a single ES module. It builds a view model out of observables and computeds, and hands it to applyBindings(viewModel, element). That is the whole integration - no hydration protocol, no second copy of the markup, no framework boot.

import {applyBindings, observable, computed} from 'domma-reactive';

applyBindings(app, document.querySelector('[data-reactive-app="bookings"]'));

The filter DSL, which is doing more work than it looks

Both demos filter server-side. The CMS turns filter[<field>_<op>]=<value> query parameters into a query the storage adapter answers - the same semantics whether the collection is JSON files or MongoDB.

Operator Meaning Example
_eq (default) equal filter[town]=Ripon
_ne not equal filter[status_ne]=cancelled
_gt _gte _lt _lte numeric or date comparison filter[price_lte]=400000
_in _nin in / not in a comma-separated list filter[propertyType_in]=Flat,Cottage
_contains _starts _ends case-insensitive substring filter[summary_contains]=garden
_exists present and non-null filter[epc_exists]=true

That matters here because a search form is a natural computed: eight observables in, one query object out, one effect watching it. The page asks the server for exactly what it is about to show.

Room booking House hunter

The rooms are photographed by Unsplash contributors and released CC0. The houses are the actual streets the listings name - Mayfield Grove, Castle Yard, Kirkgate - photographed by Geograph contributors and used under CC BY-SA. Every photograph, with its photographer and licence.