Packages
lustre
4.1.2
5.7.1
5.7.0
5.6.0
5.5.2
5.5.1
5.5.0
5.4.0
5.3.5
5.3.4
5.3.3
5.3.2
5.3.1
5.3.0
5.2.1
5.2.0
5.1.1
5.1.0
5.0.3
5.0.2
5.0.1
5.0.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.0
4.0.0-rc1
4.0.0-rc.2
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
2.0.1
2.0.0
1.3.0
1.2.0
1.1.0
1.0.0
Create HTML templates, single page applications, Web Components, and real-time server components in Gleam!
Current section
Files
Jump to
Current section
Files
src/client-runtime.ffi.mjs
import { ElementNotFound, NotABrowser } from "./lustre.mjs";
import { Dispatch, Shutdown } from "./lustre/internals/runtime.mjs";
import { morph } from "./vdom.ffi.mjs";
import { Ok, Error, isEqual } from "./gleam.mjs";
export class LustreClientApplication {
#root = null;
#queue = [];
#effects = [];
#didUpdate = false;
#isComponent = 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(selector));
const app = new LustreClientApplication(init(flags), update, view, root);
return new Ok((msg) => app.send(msg));
}
constructor(
[model, effects],
update,
view,
root = document.body,
isComponent = false,
) {
this.#model = model;
this.#update = update;
this.#view = view;
this.#root = root;
this.#effects = effects.all.toArray();
this.#didUpdate = true;
this.#isComponent = isComponent;
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();
const vdom = this.#view(this.#model);
const dispatch = (handler) => (e) => {
const result = handler(e);
if (result instanceof Ok) {
this.send(new Dispatch(result[0]));
}
};
this.#didUpdate = false;
this.#root = morph(this.#root, vdom, dispatch, this.#isComponent);
}
#flush_queue(iterations = 0) {
while (this.#queue.length) {
const [next, effects] = this.#update(this.#model, this.#queue.shift());
this.#didUpdate ||= !isEqual(this.#model, next);
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();