UNPKG

9.64 kBJavaScriptView Raw
1import { TEXT, ATTRIBUTE, INPUT, EVENT, REPEAT } from "./constants.js"
2import { updateFormControl } from "./formControl.js"
3import {
4 debounce,
5 fragmentFromTemplate,
6 getValueAtPath,
7 last,
8 walk,
9} from "./helpers.js"
10import { compareKeyedLists, getBlocks, parseEach, updateList } from "./list.js"
11import { getParts, getValueFromParts, hasMustache } from "./token.js"
12import { applyAttribute } from "./attribute.js"
13import { createContext } from "./context.js"
14
15const HYDRATE_ATTR = "mosaic-hydrate"
16
17export const render = (
18 target,
19 { getState, dispatch },
20 template,
21 updatedCallback,
22 beforeMountCallback
23) => {
24 let observer = () => {
25 let subscribers = new Set()
26 return {
27 publish: (cb) => {
28 for (let fn of subscribers) {
29 fn()
30 }
31 cb?.()
32 },
33 subscribe(fn) {
34 subscribers.add(fn)
35 },
36 }
37 }
38
39 const _ = (a, b) => (a === undefined ? b : a)
40
41 const createSubscription = {
42 [TEXT]: ({ value, node, context }, { getState }) => {
43 return {
44 handler: () => {
45 let state = context ? context.wrap(getState()) : getState()
46 let a = node.textContent
47 let b = getValueFromParts(state, getParts(value))
48 if (a !== b) node.textContent = b
49 },
50 }
51 },
52 [ATTRIBUTE]: ({ value, node, name, context }, { getState }) => {
53 return {
54 handler: () => {
55 let state = context ? context.wrap(getState()) : getState()
56 let b = getValueFromParts(state, getParts(value))
57
58 applyAttribute(node, name, b)
59
60 if (node.nodeName === "OPTION") {
61 let path = node.parentNode.getAttribute("name")
62 let selected = getValueAtPath(path, state)
63 node.selected = selected === b
64 }
65 },
66 }
67 },
68 [INPUT]: ({ node, path, context }, { getState, dispatch }) => {
69 node.addEventListener("input", () => {
70 let value =
71 node.getAttribute("type") === "checkbox" ? node.checked : node.value
72
73 if (value.trim?.().length && !isNaN(value)) value = +value
74
75 if (context) {
76 let state = context.wrap(getState())
77 state[path] = value
78 dispatch({
79 type: "MERGE",
80 payload: state,
81 })
82 } else {
83 dispatch({
84 type: "SET",
85 payload: {
86 name: path,
87 value,
88 context,
89 },
90 })
91 }
92 })
93
94 return {
95 handler: () => {
96 let state = context ? context.wrap(getState()) : getState()
97 updateFormControl(node, getValueAtPath(path, state))
98 },
99 }
100 },
101 [EVENT]: (
102 { node, eventType, actionType, context },
103 { dispatch, getState }
104 ) => {
105 /*
106
107 NB that context is only passed for local actions not prefixed with "$"
108
109 */
110
111 node.addEventListener(eventType, (event) => {
112 let isGlobal = actionType.startsWith("$")
113
114 let action = {
115 type: actionType,
116 event,
117 }
118
119 if (!isGlobal)
120 action.context = context ? context.wrap(getState()) : getState()
121
122 dispatch(action)
123
124 if (isGlobal) {
125 node.dispatchEvent(
126 new CustomEvent(actionType, {
127 detail: action,
128 bubbles: true,
129 })
130 )
131 }
132 })
133 return {
134 handler: () => {},
135 }
136 },
137 [REPEAT]: (
138 {
139 node,
140 context,
141 map,
142 path,
143 identifier,
144 index,
145 key,
146 blockIndex,
147 hydrate,
148 pickupNode,
149 },
150 { getState }
151 ) => {
152 let oldValue
153 node.$t = blockIndex - 1
154
155 const initialiseBlock = (rootNode, i, k, exitNode) => {
156 walk(
157 rootNode,
158 multi(
159 (node) => {
160 if (node === exitNode) return false
161 },
162 bindAll(
163 map,
164 hydrate,
165 createContext(
166 (context?.get() || []).concat({
167 path,
168 identifier,
169 key,
170 index,
171 i,
172 k,
173 })
174 )
175 ),
176 (child) => (child.$t = blockIndex)
177 )
178 )
179 }
180
181 function firstChild(v) {
182 return (v.nodeType === v.DOCUMENT_FRAGMENT_NODE && v.firstChild) || v
183 }
184
185 const createListItem = (datum, i) => {
186 let k = datum[key]
187 let frag = fragmentFromTemplate(node)
188 initialiseBlock(firstChild(frag), i, k)
189 return frag
190 }
191
192 if (hydrate) {
193 let x = getValueAtPath(path, getState())
194 let blocks = getBlocks(node)
195
196 blocks.forEach((block, i) => {
197 let datum = x[i]
198 let k = datum?.[key]
199 initialiseBlock(block[0], i, k, last(block).nextSibling)
200 })
201
202 pickupNode = last(last(blocks)).nextSibling
203 }
204
205 return {
206 handler: () => {
207 let state = context ? context.wrap(getState()) : getState()
208
209 const newValue = Object.entries(getValueAtPath(path, state) || [])
210 const delta = compareKeyedLists(key, oldValue, newValue)
211
212 if (delta) {
213 updateList(node, delta, newValue, createListItem)
214 }
215 oldValue = newValue.slice(0)
216 },
217 pickupNode,
218 }
219 },
220 }
221
222 const mediator = () => {
223 const o = observer()
224 return {
225 bind(v) {
226 let s = createSubscription[v.type](v, { getState, dispatch })
227 o.subscribe(s.handler)
228 return s
229 },
230 // scheduleUpdate: debounce((state, cb) => {
231 // return o.publish(state, cb)
232 // }),
233 update(cb) {
234 return o.publish(cb)
235 },
236 }
237 }
238
239 const { bind, update } = mediator()
240
241 let blockCount = 0
242
243 const parse = (frag) => {
244 let index = 0
245 let map = {}
246
247 walk(frag, (node) => {
248 let x = []
249 let pickupNode
250 switch (node.nodeType) {
251 case node.TEXT_NODE: {
252 let value = node.textContent
253 if (hasMustache(value)) {
254 x.push({
255 type: TEXT,
256 value,
257 })
258 }
259 break
260 }
261 case node.ELEMENT_NODE: {
262 let each = parseEach(node)
263
264 if (each) {
265 let ns = node.namespaceURI
266 let m
267
268 if (ns.endsWith("/svg")) {
269 node.removeAttribute("each")
270 let tpl = document.createElementNS(ns, "defs")
271 tpl.innerHTML = node.outerHTML
272 node.parentNode.replaceChild(tpl, node)
273 node = tpl
274 m = parse(node.firstChild)
275 } else {
276 if (node.nodeName !== "TEMPLATE") {
277 node.removeAttribute("each")
278 let tpl = document.createElement("template")
279
280 tpl.innerHTML = node.outerHTML
281 node.parentNode.replaceChild(tpl, node)
282 node = tpl
283 }
284 m = parse(node.content.firstChild)
285 }
286
287 pickupNode = node.nextSibling
288
289 x.push({
290 type: REPEAT,
291 map: m,
292 blockIndex: blockCount++,
293 ...each,
294 pickupNode,
295 })
296
297 break
298 }
299
300 let attrs = node.attributes
301 let i = attrs.length
302 while (i--) {
303 let { name, value } = attrs[i]
304
305 if (
306 name === ":name" &&
307 value &&
308 (node.nodeName === "INPUT" ||
309 node.nodeName === "SELECT" ||
310 node.nodeName === "TEXTAREA")
311 ) {
312 x.push({
313 type: INPUT,
314 path: value,
315 })
316
317 node.removeAttribute(name)
318 node.setAttribute("name", value)
319 } else if (name.startsWith(":on")) {
320 node.removeAttribute(name)
321 let eventType = name.split(":on")[1]
322 x.push({
323 type: EVENT,
324 eventType,
325 actionType: value,
326 })
327 } else if (name.startsWith(":")) {
328 let prop = name.slice(1)
329
330 let v = value || prop
331
332 if (!v.includes("{{")) v = `{{${v}}}`
333
334 x.push({
335 type: ATTRIBUTE,
336 name: prop,
337 value: v,
338 })
339 node.removeAttribute(name)
340 }
341 }
342 }
343 }
344 if (x.length) map[index] = x
345 index++
346 return pickupNode
347 })
348
349 return map
350 }
351
352 const multi =
353 (...fns) =>
354 (...args) => {
355 for (let fn of fns) {
356 let v = fn(...args)
357 if (v === false) return false
358 }
359 }
360
361 const bindAll = (bMap, hydrate = 0, context) => {
362 let index = 0
363 return (node) => {
364 let k = index
365 let p
366 if (k in bMap) {
367 bMap[k].forEach((v) => {
368 let x = bind({
369 ...v,
370 node,
371 context,
372 hydrate,
373 })
374 p = x.pickupNode
375 })
376 node.$i = index
377 }
378 index++
379 return p
380 }
381 }
382
383 let frag = fragmentFromTemplate(template)
384 let map = parse(frag)
385 let hydrate = target.hasAttribute?.(HYDRATE_ATTR)
386 if (hydrate) {
387 walk(target, bindAll(map, 1))
388 } else {
389 walk(frag, bindAll(map))
390 beforeMountCallback?.(frag)
391 target.prepend(frag)
392 update()
393 target.setAttribute?.(HYDRATE_ATTR, 1)
394 }
395
396 return debounce(() => update(updatedCallback))
397}