aboutsummaryrefslogtreecommitdiff
path: root/src/server-component.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/server-component.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/server-component.mjs')
-rw-r--r--src/server-component.mjs155
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);