aboutsummaryrefslogtreecommitdiff
path: root/src/client-component.ffi.mjs
blob: ce8607b7752e0594d75cfa05b784baffcaa5fcbb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { Ok, Error, isEqual } from "./gleam.mjs";
import { Dispatch, Shutdown } from "./lustre/internals/runtime.mjs";
import {
  ComponentAlreadyRegistered,
  BadComponentName,
  NotABrowser,
} from "./lustre.mjs";
import { LustreClientApplication, is_browser } from "./client-runtime.ffi.mjs";

export function register({ init, update, view, on_attribute_change }, name) {
  if (!is_browser()) return new Error(new NotABrowser());
  if (!name.includes("-")) return new Error(new BadComponentName(name));
  if (window.customElements.get(name)) {
    return new Error(new ComponentAlreadyRegistered(name));
  }

  const Component = makeComponent(init, update, view, on_attribute_change);

  window.customElements.define(name, Component);

  for (const el of document.querySelectorAll(name)) {
    const replaced = new Component();

    for (const attr of el.attributes) {
      replaced.setAttribute(attr.name, attr.value);
    }

    el.replaceWith(replaced);
  }

  return new Ok(undefined);
}

function makeComponent(init, update, view, on_attribute_change) {
  return class LustreClientComponent extends HTMLElement {
    #root = document.createElement("div");
    #application = null;
    #shadow = null;

    slotContent = [];

    static get observedAttributes() {
      return on_attribute_change[0]?.entries().map(([name, _]) => name) ?? [];
    }

    constructor() {
      super();
      this.#shadow = this.attachShadow({ mode: "closed" });

      on_attribute_change[0]?.forEach((decoder, name) => {
        Object.defineProperty(this, name, {
          get() {
            return this[`_${name}`] || this.getAttribute(name);
          },

          set(value) {
            const prev = this[name];
            const decoded = decoder(value);

            if (decoded instanceof Ok && !isEqual(prev, value)) {
              this.#application
                ? this.#application.send(new Dispatch(decoded[0]))
                : window.requestAnimationFrame(() =>
                    this.#application.send(new Dispatch(decoded[0])),
                  );
            }

            this[`_${name}`] = value;
          },
        });
      });
    }

    connectedCallback() {
      const sheet = new CSSStyleSheet();

      for (const { cssRules } of document.styleSheets) {
        for (const rule of cssRules) {
          sheet.insertRule(rule.cssText);
        }
      }

      this.#shadow.adoptedStyleSheets = [sheet];
      this.#application = new LustreClientApplication(
        init(),
        update,
        view,
        this.#root,
        true,
      );
      this.#shadow.append(this.#root);
    }

    attributeChangedCallback(key, _, next) {
      this[key] = next;
    }

    disconnectedCallback() {
      this.#application.send(new Shutdown());
    }

    get adoptedStyleSheets() {
      return this.#shadow.adoptedStyleSheets;
    }

    set adoptedStyleSheets(value) {
      this.#shadow.adoptedStyleSheets = value;
    }
  };
}