diff options
Diffstat (limited to 'src/lustre.ffi.mjs')
-rw-r--r-- | src/lustre.ffi.mjs | 224 |
1 files changed, 0 insertions, 224 deletions
diff --git a/src/lustre.ffi.mjs b/src/lustre.ffi.mjs deleted file mode 100644 index 918f7e2..0000000 --- a/src/lustre.ffi.mjs +++ /dev/null @@ -1,224 +0,0 @@ -import { - AppAlreadyStarted, - AppNotYetStarted, - BadComponentName, - ComponentAlreadyRegistered, - ElementNotFound, - NotABrowser, -} from "./lustre.mjs"; -import { from } from "./lustre/effect.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, flags) { - if (!is_browser()) return new Error(new NotABrowser()); - if (this.#root) return new Error(new AppAlreadyStarted()); - - this.#root = - selector instanceof HTMLElement - ? selector - : document.querySelector(selector); - - if (!this.#root) return new Error(new ElementNotFound()); - - const [next, effects] = this.#init(flags); - - this.#state = next; - this.#effects = effects.all.toArray(); - this.#didUpdate = true; - - window.requestAnimationFrame(() => this.#tick()); - - return new Ok((msg) => this.dispatch(msg)); - } - - 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() { - if (!this.#root) return new Error(new AppNotYetStarted()); - - this.#root.remove(); - this.#root = null; - this.#state = null; - this.#queue = []; - this.#effects = []; - this.#didUpdate = false; - this.#update = () => {}; - this.#view = () => {}; - } - - #tick() { - this.#flush(); - - if (this.#didUpdate) { - const vdom = this.#view(this.#state); - - this.#root = morph(this.#root, vdom, (msg) => this.dispatch(msg)); - this.#didUpdate = false; - } - } - - #flush(times = 0) { - if (!this.#root) return; - if (this.#queue.length) { - while (this.#queue.length) { - const [next, effects] = this.#update(this.#state, this.#queue.shift()); - // If the user returned their model unchanged and not reconstructed then - // we don't need to trigger a re-render. - this.#didUpdate ||= this.#state !== next; - this.#state = next; - this.#effects = this.#effects.concat(effects.all.toArray()); - } - } - - // Each update can produce effects which must now be executed. - while (this.#effects.length) - 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, flags) => app.start(selector, flags); -export const destroy = (app) => app.destroy(); - -// 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 (!name.includes("-")) return new Error(new BadComponentName()); - if (!is_browser()) return new Error(new NotABrowser()); - 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); - // This is necessary for ✨ reasons ✨. Clearly there's a bug in the - // implementation of either the `App` or the runtime but I con't work it - // out. - // - // If we pass the container to the app directly then the component fails - // to render anything to the ODM. - this.#container.appendChild(document.createElement("div")); - - const dispatch = this.#app.start(this.#container.firstChild); - 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, value)) { - this.#dispatch(decoded[0]); - } - - if (typeof value === "string") { - this.setAttribute(name, value); - } else { - this[`_${name}`] = value; - } - }, - }); - }); - } - - connectedCallback() { - this.appendChild(this.#container.firstChild); - } - - attributeChangedCallback(name, prev, next) { - if (prev !== next) { - this[name] = next; - } - } - - disconnectedCallback() { - this.#app.destroy(); - } - } - ); - - return new Ok(null); -}; - -// UTLS ------------------------------------------------------------------------ - -export const is_browser = () => window && window.document; -export const is_registered = (name) => !!customElements.get(name); |