GitHub - jbaicoianu/elation: Elation Web framework (original) (raw)

Elation Elements

Lightweight JavaScript component framework built with HTML custom elements, with an ergonomics layer that makes them easier to author and nicer to use. Ships a library of UI elements — buttons, inputs, tabs, lists, windows, wizards — plus data collection classes for binding them to live data sources.

On top of the standard custom element API, Elation layers a typed attribute system (int, float, boolean, vector2, etc.) with automatic attribute ↔ property coercion, lifecycle hooks (init / create / render) that fire at the right time relative to child parsing, inheritance via elation.elements.base, and an elation.elements.create() constructor that accepts an args object in place of the usual document.createElement + setAttribute dance.

Installation

Script tag

The fastest path is a CDN include — one script, one stylesheet, every element registered:

That's the whole setup. Once the bundle loads it registers every UI element and collection class with the browser's custom-element registry; markup like <ui-button> and <ui-tabs> works from then on. unpkg also serves the same artifact at https://unpkg.com/elation/build/elation.js.

elation.css is the structural layer — it gives every component the layout, dimensions, and event behaviour it needs to function, but no colours or fonts. With just elation.css loaded, components look like a plain HTML page; pair it with one of the theme files in build/themes/ (or your own) for the visual polish.

npm

The package's main field resolves to the pre-bundled build/elation.js, so a bundler-aware project can just import the side-effecting bundle:

import 'elation'; import 'elation/build/elation.css';

For a non-bundler project that imports straight from node_modules:

Building only what you need

The pre-bundled elation.js registers the entire library — roughly 430KB un-minified. Production projects typically use a subset, so Elation ships a dependency-graph packer (pack.js) that resolves only the modules you actually reference. The library's own scripts/build.sh is a thin wrapper around it — copy that script as a starting point, then change the module list at the bottom to match your slice:

node htdocs/scripts/utils/pack.js -bundle myapp
elements.ui.button elements.ui.input elements.collection.jsonapi

pack.js walks the elation.require() graph starting from each module you list, gathers every transitive dependency, and emits a single bundle plus its CSS sidecar. This is the same pattern projects like janusweb use to ship a custom build of just the elements they need.

Namespaces

Creating an element

Use them in markup:

Content for tab one Content for tab two

Or from JavaScript, either via the regular DOM API:

const tabs = document.createElement('ui-tabs'); document.body.appendChild(tabs);

…or via elation.elements.create(), which takes an args object mirroring the element's attributes (and can append in the same step):

const input = elation.elements.create('ui-input', { placeholder: 'Name', value: 'Ada', append: document.body, });

The args table on each class page lists the keys accepted by elation.elements.create(); each key also maps to an HTML attribute of the same name.

See the demo gallery for live examples of every element.

Defining new elements

Register a class with elation.elements.define(name, class). Dots in the name become dashes for the HTML tag: 'ui.counter' registers <ui-counter> and exposes the class at elation.elements.ui.counter.

elation.require(['elements.base'], function() { elation.elements.define('ui.counter', class extends elation.elements.base { init() { super.init(); this.defineAttributes({ value: { type: 'int', default: 0, set: this.updateDisplay }, step: { type: 'int', default: 1 }, }); } create() { this.addEventListener('click', () => this.value += this.step); } updateDisplay() { this.innerHTML = this.value; this.dispatchEvent({ type: 'change', data: this.value }); } }); });

Lifecycle

Three hooks, called in order:

Attributes

defineAttributes({ name: descriptor }) bridges HTML attributes ↔ JS properties automatically. Descriptor keys:

Setting el.value = 42 from JS writes back to the HTML attribute, and el.setAttribute('value', '42') or <ui-counter value="42"> populates the typed property.

Inheritance

Extend elation.elements.base for standalone elements, or an existing element class (elation.elements.ui.list, elation.elements.ui.button, elation.elements.collection.simple, …) to build on its behavior. See the sidebar for available base classes.

Type system

HTML attributes are always strings; JS properties aren't. Elation's type system bridges the two: declare a type on an attribute and reads and writes coerce transparently. It's what makes <ui-counter value="42"> produce an element whose .value is the number 42, not the string "42" — and assigning el.value = 43 from JS writes value="43" back to the markup in the same shape.

The same declaration drives the set: and get: hooks from the previous section, which fire whenever a property is read or written. set: is how an element reacts to its own attribute changes without writing a separate attributeChangedCallback.

Built-in types

The four numeric / function / boolean types coerce in the type-system switch with no external dependencies:

One additional type ships pre-registered via registerType() (covered below):

Unrecognized type names (including string, object, array) pass through with no coercion. That's harmless for string — attribute values are strings already — but for object and array it means markup attributes don't parse; only direct JS property assignment round-trips correctly. Declaring those types is still useful as a documentation hint and so editors and the docs generator can display the intent.

Registering new types

Anything richer — vectors, colors, URLs, dates — gets registered via elation.elements.registerType(name, handler). The handler is a { read, write } pair: read turns the raw attribute string into the typed value, and write turns the typed value back into a string for setAttribute. Core itself uses this to register anchor; consumers register types that depend on libraries core doesn't ship.

// Anchor is registered in core, alongside ui.panel — the hybrid type // used for edge-snap attributes. Returns true for presence with no value, // or a number when an explicit pixel offset is supplied. elation.elements.registerType('anchor', { read(value) { if (value === true || value === '' || value === 'true') return true; if (value === false || value == null || value === 'false') return false; const n = Number(value); if (isNaN(n) || n === 0) return true; return n | 0; }, write(value) { if (value === true) return ''; if (value === false || value == null) return 'false'; return String(value); } });

// And in a project that ships Three.js (e.g., Elation Engine), a // downstream registration that depends on a runtime core won't pull in: elation.elements.registerType('vector3', { read(value) { if (value instanceof THREE.Vector3) return value; const [x, y, z] = ('' + value).split(/\s+/).map(Number); return new THREE.Vector3(x, y, z); }, write(value) { return ${value.x} <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>v</mi><mi>a</mi><mi>l</mi><mi>u</mi><mi>e</mi><mi mathvariant="normal">.</mi><mi>y</mi></mrow><annotation encoding="application/x-tex">{value.y} </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span></span>{value.z}; } });

With vector3 registered, any element can declare position: { type: 'vector3' } and its .position property is a live THREE.Vector3 while the markup stays valid as <my-element position="1 2 3">. Elation core registers types it can implement without external dependencies (like anchor); richer types are the consuming project's responsibility.

Collections

Collections are the data layer of the library — a consistent interface for list-shaped data that changes over time. Each collection holds an array of items and emits collection_add, collection_remove, collection_move, and collection_clear events when its contents change. List-style UI elements (ui.list, ui.grid, ui.tabs, ui.checklist, …) accept a collection reference and re-render incrementally as those events fire — no manual subscriptions, no full re-renders.

Binding a list to data

Set the collection property on the list element to a collection instance:

const users = elation.elements.create('collection-jsonapi', { host: 'https://api.example.com', endpoint: '/users', });

const list = elation.elements.create('ui-list', { collection: users, append: document.body, }); // list populates when users loads, and updates whenever // users.add() / .remove() / .move() fires.

For one-off load completion, listen on the collection directly:

elation.events.add(users, 'collection_load', () => { console.log(fetched ${users.items.length} users); });

What ships in core

The class family covers three patterns of data backing, all built on the same event interface so any of them can drive any list element:

Plus three derivers that expose a transformed view of another collection:

See the collection namespace in the API docs for the full per-class attribute and event reference.

License

MIT — see LICENSE.