UNPKG

4.02 kBJavaScriptView Raw
1import { configure } from "./update.js"
2import { render } from "./render.js"
3import { mergeSlots } from "./mergeSlots.js"
4import {
5 applyAttribute,
6 attributeToProp,
7 isPrimitive,
8 pascalToKebab,
9} from "./helpers.js"
10
11function getDataScript(node) {
12 return node.querySelector(`script[type="application/mosaic"]`)
13}
14
15function createDataScript(node) {
16 let ds = document.createElement("script")
17 ds.setAttribute("type", "application/mosaic")
18 node.append(ds)
19 return ds
20}
21
22function serialise(node, state) {
23 let ds = getDataScript(node) || createDataScript(node)
24
25 ds.innerText = JSON.stringify(state)
26}
27
28function deserialise(node) {
29 return JSON.parse(getDataScript(node)?.innerText || "{}")
30}
31
32let count = 0
33
34function nextId() {
35 return count++
36}
37
38export const define = (name, factory, template) =>
39 customElements.define(
40 name,
41 class extends HTMLElement {
42 async connectedCallback() {
43 if (!this.initialised) {
44 let config = factory(this)
45 const slice = `name.${nextId()}`
46
47 if (config instanceof Promise) config = await config
48
49 let {
50 update,
51 middleware,
52 derivations,
53 subscribe,
54 shadow,
55 initialState = {},
56 } = config
57
58 this.connectedCallback = config.connectedCallback
59 this.disconnectedCallback = config.disconnectedCallback
60
61 const { dispatch, getState, onUpdate, flush } = configure(
62 config,
63 this
64 )
65
66 dispatch({
67 type: "MERGE",
68 payload: deserialise(this),
69 })
70
71 initialState = getState()
72
73 let observedProps = Object.keys(initialState).filter(
74 (v) => v.charAt(0) === "$"
75 )
76
77 let observedAttributes = observedProps
78 .map((v) => v.slice(1))
79 .map(pascalToKebab)
80
81 let sa = this.setAttribute
82 this.setAttribute = (k, v) => {
83 if (observedAttributes.includes(k)) {
84 let { name, value } = attributeToProp(k, v)
85 dispatch({
86 type: "SET",
87 payload: { name: "$" + name, value },
88 })
89 }
90 sa.apply(this, [k, v])
91 }
92
93 observedAttributes.forEach((name) => {
94 let property = attributeToProp(name).name
95
96 let value
97
98 if (this.hasAttribute(name)) {
99 value = this.getAttribute(name)
100 } else {
101 value = this[property] || initialState["$" + property]
102 }
103
104 Object.defineProperty(this, property, {
105 get() {
106 return getState()["$" + property]
107 },
108 set(v) {
109 dispatch({
110 type: "SET",
111 payload: { name: "$" + property, value: v },
112 })
113 if (isPrimitive(v)) {
114 applyAttribute(this, property, v)
115 }
116 },
117 })
118
119 this[property] = value
120 })
121
122 let beforeMountCallback
123
124 if (shadow) {
125 this.attachShadow({
126 mode: shadow,
127 })
128 } else {
129 beforeMountCallback = (frag) => mergeSlots(this, frag)
130 }
131
132 onUpdate(
133 render(
134 this.shadowRoot || this,
135 { getState, dispatch },
136 template,
137 () => {
138 serialise(this, getState())
139
140 observedProps.forEach((k) => {
141 let v = getState()[k]
142 if (isPrimitive(v)) applyAttribute(this, k.slice(1), v)
143 })
144 subscribe?.(getState())
145 flush()
146 },
147 beforeMountCallback
148 )
149 )
150 this.initialised = true
151 }
152 this.connectedCallback?.()
153 }
154 disconnectedCallback() {
155 this.disconnectedCallback?.()
156 }
157 }
158 )