aboutsummaryrefslogtreecommitdiff
path: root/src/client-runtime.ffi.mjs
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2024-01-23 00:09:45 +0000
committerGitHub <noreply@github.com>2024-01-23 00:09:45 +0000
commit24f6962aa457d32319756f6217aafde7b0a9c752 (patch)
tree42119d9b073f56eabe9dda4ae2065ef4b2086e6a /src/client-runtime.ffi.mjs
parent45e671ac32de95ae1a0a9f9e98da8645d01af3cf (diff)
downloadlustre-24f6962aa457d32319756f6217aafde7b0a9c752.tar.gz
lustre-24f6962aa457d32319756f6217aafde7b0a9c752.zip
✨ Add universal components that can run on the server (#39)
* :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.
Diffstat (limited to 'src/client-runtime.ffi.mjs')
-rw-r--r--src/client-runtime.ffi.mjs133
1 files changed, 133 insertions, 0 deletions
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();