diff options
Diffstat (limited to 'src/lustre.ffi.mjs')
-rw-r--r-- | src/lustre.ffi.mjs | 206 |
1 files changed, 0 insertions, 206 deletions
diff --git a/src/lustre.ffi.mjs b/src/lustre.ffi.mjs deleted file mode 100644 index b99f6e2..0000000 --- a/src/lustre.ffi.mjs +++ /dev/null @@ -1,206 +0,0 @@ -import { ElementNotFound, ComponentAlreadyRegistered } from "./lustre.mjs"; -import { from } from "./lustre/effect.mjs"; -import { map } from "./lustre/element.mjs"; -import { morph } from "./runtime.ffi.mjs"; -import { Ok, Error, isEqual } from "./gleam.mjs"; - -// RUNTIME --------------------------------------------------------------------- - -/// -/// -export class App { - #root = null; - #state = null; - #queue = []; - #effects = []; - #didUpdate = false; - - #init = null; - #update = null; - #view = null; - - constructor(init, update, render) { - this.#init = init; - this.#update = update; - this.#view = render; - } - - start(selector = "body") { - if (this.#root) return this; - - try { - const el = - selector instanceof HTMLElement - ? selector - : document.querySelector(selector); - const [next, effects] = this.#init(); - - this.#root = el; - this.#state = next; - this.#effects = effects[0].toArray(); - this.#didUpdate = true; - - window.requestAnimationFrame(() => this.#tick()); - - return new Ok((msg) => this.dispatch(msg)); - } catch (_) { - return new Error(new ElementNotFound()); - } - } - - dispatch(msg) { - this.#queue.push(msg); - this.#tick(); - } - - emit(name, event = null) { - this.#root.dispatchEvent( - new CustomEvent(name, { - bubbles: true, - detail: event, - composed: true, - }) - ); - } - - destroy() { - this.#root.remove(); - this.#state = null; - this.#queue = []; - this.#effects = []; - this.#didUpdate = false; - this.#update = () => {}; - this.#view = () => {}; - } - - #render() { - const node = this.#view(this.#state); - const vdom = map(node, (msg) => this.dispatch(msg)); - - morph(this.#root, vdom); - } - - #tick() { - this.#flush(); - this.#didUpdate && this.#render(); - this.#didUpdate = false; - } - - #flush(times = 0) { - if (this.#queue.length) { - while (this.#queue.length) { - const [next, effects] = this.#update(this.#state, this.#queue.shift()); - - this.#state = next; - this.#effects = this.#effects.concat(effects[0].toArray()); - } - this.#didUpdate = true; - } - - // Each update can produce effects which must now be executed. - while (this.#effects[0]) - this.#effects.shift()( - (msg) => this.dispatch(msg), - (name, data) => this.emit(name, data) - ); - - // Synchronous effects will immediately queue a message to be processed. If - // it is reasonable, we can process those updates too before proceeding to - // the next render. - if (this.#queue.length) { - times >= 5 ? console.warn(tooManyUpdates) : this.#flush(++times); - } - } -} - -export const setup = (init, update, render) => new App(init, update, render); -export const start = (app, selector) => app.start(selector); - -export const emit = (name, data) => - // Normal `Effect`s constructed in Gleam from `effect.from` don't get told - // about the second argument, but it's there 👀. - from((_, emit) => { - emit(name, data); - }); - -// HTML EVENTS ----------------------------------------------------------------- - -export const prevent_default = (e) => e.preventDefault?.(); -export const stop_propagation = (e) => e.stopPropagation?.(); - -// CUSTOM ELEMENTS ------------------------------------------------------------- - -export const setup_component = ( - name, - init, - update, - render, - on_attribute_change -) => { - if (customElements.get(name)) { - return new Error(new ComponentAlreadyRegistered()); - } - - customElements.define( - name, - class extends HTMLElement { - static get observedAttributes() { - return on_attribute_change.entries().map(([name, _]) => name); - } - - #container = document.createElement("div"); - #app = null; - #dispatch = null; - - constructor() { - super(); - - this.#app = new App(init, update, render); - const dispatch = this.#app.start(this.#container); - this.#dispatch = dispatch[0]; - - on_attribute_change.forEach((decoder, name) => { - Object.defineProperty(this, name, { - get: () => { - return this[`_${name}`] || this.getAttribute(name); - }, - - set: (value) => { - const prev = this[name]; - const decoded = decoder(value); - - // We need this equality check to prevent constantly dispatching - // messages when the value is an object or array: it might not have - // changed but its reference might have and we don't want to trigger - // useless updates. - if (decoded.isOk() && !isEqual(prev, decoded[0])) { - this.#dispatch(decoded[0]); - } - - if (typeof value === "string") { - this.setAttribute(name, value); - } else { - this[`_${name}`] = value; - } - }, - }); - }); - } - - connectedCallback() { - this.appendChild(this.#container); - } - - attributeChangedCallback(name, prev, next) { - if (prev !== next) { - this[name] = next; - } - } - - disconnectedCallback() { - this.#app.destroy(); - } - } - ); - return new Ok(null); -}; |