From 24f6962aa457d32319756f6217aafde7b0a9c752 Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Tue, 23 Jan 2024 00:09:45 +0000 Subject: =?UTF-8?q?=E2=9C=A8=20Add=20universal=20components=20that=20can?= =?UTF-8?q?=20run=20on=20the=20server=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :heavy_plus_sign: Add gleam_erlang gleam_otp and gleam_json dependencies. * :sparkles: Add json encoders for elememnts and attributes. * :sparkles: Add the ability to perform an effect with a custom dispatch function. * :construction: Experiment with a server-side component runtime. * :construction: Expose special server click events. * :construction: Experiment with a server-side component runtime. * :construction: Experiment with a server-side component runtime. * :construction: Experiment with a server-side component runtime. * :construction: Create a basic server component client bundle. * :construction: Create a basic server component demo. * :bug: Fixed a bug where the runtime stopped performing patches. * :refactor: Roll back introduction of shadow dom. * :recycle: Refactor to Custom Element-based approach to encapsulating server components. * :truck: Move some things around. * :sparkles: Add a minified version of the server component runtime. * :wrench: Add lustre/server/* to internal modules. * :recycle: on_attribute_change and on_client_event handlers are now functions not dicts. * :recycle: Refactor server component event handling to no longer need explicit tags. * :fire: Remove unnecessary attempt to stringify events. * :memo: Start documeint lustre/server functions. * :construction: Experiment with a js implementation of the server component backend runtime. * :recycle: Experiment with an API that makes heavier use of conditional complilation. * :recycle: Big refactor to unify server components, client components, and client apps. * :bug: Fixed some bugs with client runtimes. * :recycle: Update examples to new lustre api/ * :truck: Move server demo into examples/ folder/ * :wrench: Add lustre/runtime to internal modules. * :construction: Experiment with a diffing implementation. * :wrench: Hide internal modules from docs. * :heavy_plus_sign: Update deps to latest versions. * :recycle: Move diffing and vdom code into separate internal modules. * :sparkles: Bring server components to feature parity with client components. * :recycle: Update server component demo. * :bug: Fix bug where attribute changes weren't properly broadcast. * :fire: Remove unused 'Patch' type. * :recycle: Stub out empty js implementations so we can build for js. * :memo: Docs for the docs gods. * :recycle: Rename lustre.server_component to lustre.component. --- src/client-runtime.ffi.mjs | 133 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/client-runtime.ffi.mjs (limited to 'src/client-runtime.ffi.mjs') diff --git a/src/client-runtime.ffi.mjs b/src/client-runtime.ffi.mjs new file mode 100644 index 0000000..13e3906 --- /dev/null +++ b/src/client-runtime.ffi.mjs @@ -0,0 +1,133 @@ +import { ElementNotFound, NotABrowser } from "./lustre.mjs"; +import { Dispatch, Shutdown } from "./lustre/runtime.mjs"; +import { morph } from "./vdom.ffi.mjs"; +import { Ok, Error, isEqual } from "./gleam.mjs"; + +export class LustreClientApplication { + #root = null; + #queue = []; + #effects = []; + #didUpdate = false; + + #model = null; + #update = null; + #view = null; + + static start(flags, selector, init, update, view) { + if (!is_browser()) return new Error(new NotABrowser()); + const root = + selector instanceof HTMLElement + ? selector + : document.querySelector(selector); + if (!root) return new Error(new ElementNotFound()); + const app = new LustreClientApplication(init(flags), update, view, root); + + return new Ok((msg) => app.send(msg)); + } + + constructor([model, effects], update, view, root = document.body) { + this.#model = model; + this.#update = update; + this.#view = view; + this.#root = root; + this.#effects = effects.all.toArray(); + this.#didUpdate = true; + + window.requestAnimationFrame(() => this.#tick()); + } + + send(action) { + switch (true) { + case action instanceof Dispatch: { + this.#queue.push(action[0]); + this.#tick(); + + return; + } + + case action instanceof Shutdown: { + this.#shutdown(); + return; + } + + default: + return; + } + } + + emit(event, data) { + this.#root.dispatchEvent( + new CustomEvent(event, { + bubbles: true, + detail: data, + composed: true, + }) + ); + } + + #tick() { + this.#flush_queue(); + + if (this.#didUpdate) { + const vdom = this.#view(this.#model); + + this.#didUpdate = false; + this.#root = morph(this.#root, vdom, (msg) => { + this.send(new Dispatch(msg)); + }); + } + } + + #flush_queue(iterations = 0) { + while (this.#queue.length) { + const [next, effects] = this.#update(this.#model, this.#queue.shift()); + + this.#model = next; + this.#didUpdate ||= isEqual(this.#model, next); + this.#effects = this.#effects.concat(effects.all.toArray()); + } + + while (this.#effects.length) { + this.#effects.shift()( + (msg) => this.send(new Dispatch(msg)), + (event, data) => this.emit(event, data) + ); + } + + if (this.#queue.length) { + if (iterations < 5) { + this.#flush_queue(++iterations); + } else { + window.requestAnimationFrame(() => this.#tick()); + } + } + } + + #shutdown() { + this.#root.remove(); + this.#root = null; + this.#model = null; + this.#queue = []; + this.#effects = []; + this.#didUpdate = false; + this.#update = () => {}; + this.#view = () => {}; + } +} + +export const start = (app, selector, flags) => + LustreClientApplication.start( + flags, + selector, + app.init, + app.update, + app.view + ); + +// UTILS ----------------------------------------------------------------------- + +export const is_browser = () => window && window.document; +export const is_registered = (name) => + is_browser() && !!window.customElements.get(name); +export const prevent_default = (event) => event.preventDefault(); +export const stop_propagation = (event) => event.stopPropagation(); -- cgit v1.2.3