diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2024-01-23 00:09:45 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-23 00:09:45 +0000 |
commit | 24f6962aa457d32319756f6217aafde7b0a9c752 (patch) | |
tree | 42119d9b073f56eabe9dda4ae2065ef4b2086e6a /src/server-component.mjs | |
parent | 45e671ac32de95ae1a0a9f9e98da8645d01af3cf (diff) | |
download | lustre-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/server-component.mjs')
-rw-r--r-- | src/server-component.mjs | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/src/server-component.mjs b/src/server-component.mjs new file mode 100644 index 0000000..b7aec2c --- /dev/null +++ b/src/server-component.mjs @@ -0,0 +1,155 @@ +// Note that this path is relative to the built Gleam project, not the source files +// in `src/`. This particular module is not used by the Lustre package itself, but +// is instead bundled and made available to package users in the `priv/` directory. +// +// It makes obvious sense to co-locate the source with the rest of the package +// source code, but if we use relative imports here the bundle will fail because +// `vdom.ffi.mjs` is importing things from the Gleam standard library and expects +// to be placed in the `build/dev/javascript/lustre/` directory. +// +import * as Constants from "../build/dev/javascript/lustre/lustre/internals/constants.mjs"; +import { patch, morph } from "../build/dev/javascript/lustre/vdom.ffi.mjs"; + +export class LustreServerComponent extends HTMLElement { + static get observedAttributes() { + return ["route"]; + } + + #observer = null; + #root = null; + #socket = null; + + constructor() { + super(); + + this.#observer = new MutationObserver((mutations) => { + const changed = []; + + for (const mutation of mutations) { + if (mutation.type === "attributes") { + const { attributeName: name, oldValue: prev } = mutation; + const next = this.getAttribute(name); + + if (prev !== next) { + try { + changed.push([name, JSON.parse(next)]); + } catch { + changed.push([name, next]); + } + } + } + } + + if (changed.length) { + this.#socket?.send(JSON.stringify([Constants.attrs, changed])); + } + }); + } + + connectedCallback() { + this.#root = document.createElement("div"); + this.appendChild(this.#root); + } + + attributeChangedCallback(name, prev, next) { + switch (name) { + case "route": { + if (!next) { + this.#socket?.close(); + this.#socket = null; + } else if (prev !== next) { + const id = this.getAttribute("id"); + const route = next + (id ? `?id=${id}` : ""); + + this.#socket?.close(); + this.#socket = new WebSocket(`ws://${window.location.host}${route}`); + this.#socket.addEventListener("message", ({ data }) => { + const [kind, ...payload] = JSON.parse(data); + + switch (kind) { + case Constants.diff: + return this.diff(payload); + + case Constants.emit: + return this.emit(payload); + + case Constants.init: + return this.init(payload); + } + }); + } + } + } + } + + init([attrs, vdom]) { + const initial = []; + + for (const attr of attrs) { + if (attr in this) { + initial.push([attr, this[attr]]); + } else if (this.hasAttribute(attr)) { + initial.push([attr, this.getAttribute(attr)]); + } + + Object.defineProperty(this, attr, { + get() { + return this[`_${attr}`] ?? this.getAttribute(attr); + }, + set(value) { + const prev = this[attr]; + + if (typeof value === "string") { + this.setAttribute(attr, value); + } else { + this[`_${attr}`] = value; + } + + if (prev !== value) { + this.#socket?.send( + JSON.stringify([Constants.attrs, [[attr, value]]]) + ); + } + }, + }); + } + + this.#observer.observe(this, { + attributeFilter: attrs, + attributeOldValue: true, + attributes: true, + characterData: false, + characterDataOldValue: false, + childList: false, + subtree: false, + }); + + this.morph(vdom); + + if (initial.length) { + this.#socket?.send(JSON.stringify([Constants.attrs, initial])); + } + } + + morph(vdom) { + this.#root = morph(this.#root, vdom, (msg) => { + this.#socket?.send(JSON.stringify([Constants.event, msg.tag, msg.data])); + }); + } + + diff([diff]) { + this.#root = patch(this.#root, diff, (msg) => { + this.#socket?.send(JSON.stringify([Constants.event, msg.tag, msg.data])); + }); + } + + emit([event, data]) { + this.dispatchEvent(new CustomEvent(event, { detail: data })); + } + + disconnectedCallback() { + this.#socket?.close(); + } +} + +window.customElements.define("lustre-server-component", LustreServerComponent); |