@srfnstack/fntags 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/fntags.mjs +194 -105
package/package.json
CHANGED
package/src/fntags.mjs
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* @param children optional attrs and children for the element
|
|
11
11
|
* @returns HTMLElement an html element
|
|
12
12
|
*
|
|
13
|
-
|
|
14
13
|
*/
|
|
15
|
-
export
|
|
14
|
+
export function h () {
|
|
15
|
+
const tag = arguments[0]
|
|
16
|
+
let firstChildIdx = 1
|
|
16
17
|
let element
|
|
17
18
|
if (tag.startsWith('ns=')) {
|
|
18
19
|
element = document.createElementNS(...(tag.slice(3).split('|')))
|
|
@@ -20,18 +21,15 @@ export const h = (tag, ...children) => {
|
|
|
20
21
|
element = document.createElement(tag)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
if (isAttrs(
|
|
24
|
-
const attrs =
|
|
24
|
+
if (isAttrs(arguments[firstChildIdx])) {
|
|
25
|
+
const attrs = arguments[firstChildIdx]
|
|
26
|
+
firstChildIdx += 1
|
|
25
27
|
for (const a in attrs) {
|
|
26
|
-
|
|
27
|
-
if (typeof attr === 'function' && attr.isBoundAttribute) {
|
|
28
|
-
attr.init(a, element)
|
|
29
|
-
attr = attr()
|
|
30
|
-
}
|
|
31
|
-
setAttribute(a, attr, element)
|
|
28
|
+
setAttribute(a, attrs[a], element)
|
|
32
29
|
}
|
|
33
30
|
}
|
|
34
|
-
for (
|
|
31
|
+
for (let i = firstChildIdx; i < arguments.length; i++) {
|
|
32
|
+
const child = arguments[i]
|
|
35
33
|
if (Array.isArray(child)) {
|
|
36
34
|
for (const c of child) {
|
|
37
35
|
element.append(renderNode(c))
|
|
@@ -43,6 +41,68 @@ export const h = (tag, ...children) => {
|
|
|
43
41
|
return element
|
|
44
42
|
}
|
|
45
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
46
|
+
* defined in the template.
|
|
47
|
+
*
|
|
48
|
+
* This allows fast rendering by precreating a dom element with the entire template structure and cloning and populating
|
|
49
|
+
* the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
|
|
50
|
+
* one by one and can speed up situations where the same elements are created many times.
|
|
51
|
+
*
|
|
52
|
+
* You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
|
|
53
|
+
* not be updated when the state changes because they will not be bound to the cloned element.
|
|
54
|
+
*
|
|
55
|
+
* All state bindings must be passed to the compiled template function to work correctly.
|
|
56
|
+
* @param templateFn {function(object): Node}
|
|
57
|
+
* @return {function(*): Node}
|
|
58
|
+
*/
|
|
59
|
+
export const fntemplate = templateFn => {
|
|
60
|
+
const placeholders = { }
|
|
61
|
+
let id = 1
|
|
62
|
+
const initContext = prop => {
|
|
63
|
+
const placeholder = (element, type, attrOrStyle) => {
|
|
64
|
+
let selector = element.selector
|
|
65
|
+
if (!selector) {
|
|
66
|
+
selector = `fntpl-${prop}-${id++}`
|
|
67
|
+
element.selector = selector
|
|
68
|
+
element.classList.add(selector)
|
|
69
|
+
}
|
|
70
|
+
if (!placeholders[selector]) placeholders[selector] = []
|
|
71
|
+
placeholders[selector].push({ prop, type, attrOrStyle })
|
|
72
|
+
}
|
|
73
|
+
placeholder.isTemplatePlaceholder = true
|
|
74
|
+
return placeholder
|
|
75
|
+
}
|
|
76
|
+
// The initial render is cloned to prevent invalid state bindings from changing it
|
|
77
|
+
const rendered = templateFn(initContext).cloneNode(true)
|
|
78
|
+
return ctx => {
|
|
79
|
+
const clone = rendered.cloneNode(true)
|
|
80
|
+
for (const selectorClass in placeholders) {
|
|
81
|
+
const targetElement = clone.classList.contains(selectorClass) ? clone : clone.getElementsByClassName(selectorClass)[0]
|
|
82
|
+
targetElement.classList.remove(selectorClass)
|
|
83
|
+
for (const placeholder of placeholders[selectorClass]) {
|
|
84
|
+
if (!ctx[placeholder.prop]) {
|
|
85
|
+
console.warn(`No value provided for template prop: ${placeholder.prop}`)
|
|
86
|
+
}
|
|
87
|
+
switch (placeholder.type) {
|
|
88
|
+
case 'node':
|
|
89
|
+
targetElement.replaceWith(renderNode(ctx[placeholder.prop]))
|
|
90
|
+
break
|
|
91
|
+
case 'attr':
|
|
92
|
+
setAttribute(placeholder.attrOrStyle, ctx[placeholder.prop], targetElement)
|
|
93
|
+
break
|
|
94
|
+
case 'style':
|
|
95
|
+
setStyle(placeholder.attrOrStyle, ctx[placeholder.prop], targetElement)
|
|
96
|
+
break
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unexpected bindType ${placeholder.type}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return clone
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
46
106
|
/**
|
|
47
107
|
* Create a state object that can be bound to.
|
|
48
108
|
* @param initialValue The initial state
|
|
@@ -76,90 +136,83 @@ export const fnstate = (initialValue, mapKey) => {
|
|
|
76
136
|
}
|
|
77
137
|
|
|
78
138
|
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
* Bind the values of this state to the given element.
|
|
140
|
+
* Values are items/elements of an array.
|
|
141
|
+
* If the current value is not an array, this will behave the same as bindAs.
|
|
142
|
+
*
|
|
143
|
+
* @param parent The parent to bind the children to.
|
|
144
|
+
* @param element The element to bind to. If not a function, an update function must be passed
|
|
145
|
+
* @param update If passed this will be executed directly when the state of any value changes with no other intervention
|
|
146
|
+
*/
|
|
87
147
|
ctx.state.bindChildren = (parent, element, update) => doBindChildren(ctx, parent, element, update)
|
|
88
148
|
|
|
89
149
|
/**
|
|
90
|
-
|
|
91
|
-
*
|
|
92
|
-
* @param element The element to bind to. If not a function, an update function must be passed
|
|
93
|
-
* @param update If passed this will be executed directly when the state changes with no other intervention
|
|
94
|
-
* @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
|
|
95
|
-
*/
|
|
96
|
-
ctx.state.bindAs = (element, update) => doBindAs(ctx, element, update)
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Bind this state as it's value
|
|
150
|
+
* Bind this state to the given element
|
|
100
151
|
*
|
|
152
|
+
* @param [element] The element to bind to. If not a function, an update function must be passed. If not passed, defaults to the state's value
|
|
153
|
+
* @param [update] If passed this will be executed directly when the state changes with no other intervention
|
|
101
154
|
* @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
|
|
102
155
|
*/
|
|
103
|
-
ctx.state.
|
|
156
|
+
ctx.state.bindAs = (element, update) => doBindAs(ctx, element ?? ctx.state, update)
|
|
104
157
|
|
|
105
158
|
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
ctx.state.bindAttr = (attribute) => doBindAttr(ctx.state, attribute)
|
|
159
|
+
* Bind attribute values to state changes
|
|
160
|
+
* @param [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
161
|
+
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
162
|
+
*/
|
|
163
|
+
ctx.state.bindAttr = (attribute) => doBindAttr(ctx.state, attribute ?? ctx.state)
|
|
111
164
|
|
|
112
165
|
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
ctx.state.bindStyle = (style) => doBindStyle(ctx.state, style)
|
|
166
|
+
* Bind style values to state changes
|
|
167
|
+
* @param [style] A function that returns a style's value. If not passed, defaults to the state's value
|
|
168
|
+
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
169
|
+
*/
|
|
170
|
+
ctx.state.bindStyle = (style) => doBindStyle(ctx.state, style ?? ctx.state)
|
|
118
171
|
|
|
119
172
|
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
ctx.state.bindSelect = (element, update) => doBindSelect(ctx, element, update)
|
|
173
|
+
* Bind select and deselect to an element
|
|
174
|
+
* @param [element] The element to bind to. If not a function, an update function must be passed. If not passed, defaults to the state's value
|
|
175
|
+
* @param update If passed this will be executed directly when the state changes with no other intervention
|
|
176
|
+
*/
|
|
177
|
+
ctx.state.bindSelect = (element, update) => doBindSelect(ctx, element ?? ctx.state, update)
|
|
125
178
|
|
|
126
179
|
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
ctx.state.bindSelectAttr = (attribute) => doBindSelectAttr(ctx, attribute)
|
|
180
|
+
* Bind select and deselect to an attribute
|
|
181
|
+
* @param [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
182
|
+
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
183
|
+
*/
|
|
184
|
+
ctx.state.bindSelectAttr = (attribute) => doBindSelectAttr(ctx, attribute ?? ctx.state)
|
|
132
185
|
|
|
133
186
|
/**
|
|
134
|
-
|
|
135
|
-
|
|
187
|
+
* Mark the element with the given key as selected. This causes the bound select functions to be executed.
|
|
188
|
+
*/
|
|
136
189
|
ctx.state.select = (key) => doSelect(ctx, key)
|
|
137
190
|
|
|
138
191
|
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
192
|
+
* Get the currently selected key
|
|
193
|
+
* @returns {*}
|
|
194
|
+
*/
|
|
142
195
|
ctx.state.selected = () => ctx.selected
|
|
143
196
|
|
|
144
197
|
ctx.state.isFnState = true
|
|
145
198
|
|
|
146
199
|
/**
|
|
147
|
-
|
|
148
|
-
|
|
200
|
+
* Perform an Object.assign on the current state using the provided update
|
|
201
|
+
*/
|
|
149
202
|
ctx.state.assign = (update) => ctx.state(Object.assign(ctx.currentValue, update))
|
|
150
203
|
|
|
151
204
|
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
* Get a value at the given property path, an error is thrown if the value is not an object
|
|
206
|
+
*
|
|
207
|
+
* This returns a reference to the real current value. If you perform any modifications to the object, be sure to call setPath after you're done or the changes
|
|
208
|
+
* will not be reflected correctly.
|
|
209
|
+
*/
|
|
157
210
|
ctx.state.getPath = (path) => {
|
|
158
211
|
if (typeof path !== 'string') {
|
|
159
|
-
throw new Error('Invalid path')
|
|
212
|
+
throw new Error('Invalid path')
|
|
160
213
|
}
|
|
161
214
|
if (typeof ctx.currentValue !== 'object') {
|
|
162
|
-
throw new Error('Value is not an object')
|
|
215
|
+
throw new Error('Value is not an object')
|
|
163
216
|
}
|
|
164
217
|
return path
|
|
165
218
|
.split('.')
|
|
@@ -176,11 +229,11 @@ export const fnstate = (initialValue, mapKey) => {
|
|
|
176
229
|
}
|
|
177
230
|
|
|
178
231
|
/**
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
232
|
+
* Set a value at the given property path
|
|
233
|
+
* @param path The JSON path of the value to set
|
|
234
|
+
* @param value The value to set the path to
|
|
235
|
+
* @param fillWithObjects Whether to non object values with new empty objects.
|
|
236
|
+
*/
|
|
184
237
|
ctx.state.setPath = (path, value, fillWithObjects = false) => {
|
|
185
238
|
const s = path.split('.')
|
|
186
239
|
const parent = s
|
|
@@ -199,19 +252,19 @@ export const fnstate = (initialValue, mapKey) => {
|
|
|
199
252
|
parent[s.slice(-1)] = value
|
|
200
253
|
ctx.state(ctx.currentValue)
|
|
201
254
|
} else {
|
|
202
|
-
throw new Error(`No object at path ${path}`)
|
|
255
|
+
throw new Error(`No object at path ${path}`)
|
|
203
256
|
}
|
|
204
257
|
}
|
|
205
258
|
|
|
206
259
|
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
260
|
+
* Register a callback that will be executed whenever the state is changed
|
|
261
|
+
* @return a function to stop the subscription
|
|
262
|
+
*/
|
|
210
263
|
ctx.state.subscribe = (callback) => doSubscribe(ctx, ctx.observers, callback)
|
|
211
264
|
|
|
212
265
|
/**
|
|
213
|
-
|
|
214
|
-
|
|
266
|
+
* Remove all of the observers and optionally reset the value to it's initial value
|
|
267
|
+
*/
|
|
215
268
|
ctx.state.reset = (reInit) => doReset(ctx, reInit, initialValue)
|
|
216
269
|
|
|
217
270
|
return ctx.state
|
|
@@ -244,7 +297,7 @@ const doBindSelectAttr = function (ctx, attribute) {
|
|
|
244
297
|
|
|
245
298
|
function createBoundAttr (attr) {
|
|
246
299
|
if (typeof attr !== 'function') {
|
|
247
|
-
throw new Error('You must pass a function to bindAttr')
|
|
300
|
+
throw new Error('You must pass a function to bindAttr')
|
|
248
301
|
}
|
|
249
302
|
const boundAttr = () => attr()
|
|
250
303
|
boundAttr.isBoundAttribute = true
|
|
@@ -259,7 +312,7 @@ function doBindAttr (state, attribute) {
|
|
|
259
312
|
|
|
260
313
|
function doBindStyle (state, style) {
|
|
261
314
|
if (typeof style !== 'function') {
|
|
262
|
-
throw new Error('You must pass a function to bindStyle')
|
|
315
|
+
throw new Error('You must pass a function to bindStyle')
|
|
263
316
|
}
|
|
264
317
|
const boundStyle = () => style()
|
|
265
318
|
boundStyle.isBoundStyle = true
|
|
@@ -289,10 +342,10 @@ function doSelect (ctx, key) {
|
|
|
289
342
|
function doBindChildren (ctx, parent, element, update) {
|
|
290
343
|
parent = renderNode(parent)
|
|
291
344
|
if (parent === undefined) {
|
|
292
|
-
throw new Error('You must provide a parent element to bind the children to. aka Need Bukkit.')
|
|
345
|
+
throw new Error('You must provide a parent element to bind the children to. aka Need Bukkit.')
|
|
293
346
|
}
|
|
294
347
|
if (typeof element !== 'function' && typeof update !== 'function') {
|
|
295
|
-
throw new Error('You must pass an update function when passing a non function element')
|
|
348
|
+
throw new Error('You must pass an update function when passing a non function element')
|
|
296
349
|
}
|
|
297
350
|
if (typeof ctx.mapKey !== 'function') {
|
|
298
351
|
console.warn('Using value index as key, may not work correctly when moving items...')
|
|
@@ -327,7 +380,7 @@ function doBindChildren (ctx, parent, element, update) {
|
|
|
327
380
|
|
|
328
381
|
const doBind = function (ctx, element, update, handleUpdate, handleReplace) {
|
|
329
382
|
if (typeof element !== 'function' && typeof update !== 'function') {
|
|
330
|
-
throw new Error('You must pass an update function when passing a non function element')
|
|
383
|
+
throw new Error('You must pass an update function when passing a non function element')
|
|
331
384
|
}
|
|
332
385
|
if (typeof update === 'function') {
|
|
333
386
|
const boundElement = renderNode(evaluateElement(element, ctx.currentValue))
|
|
@@ -426,7 +479,7 @@ function arrangeElements (ctx, bindContext) {
|
|
|
426
479
|
}
|
|
427
480
|
const key = keyMapper(ctx.mapKey, valueState())
|
|
428
481
|
if (keys[key]) {
|
|
429
|
-
throw new Error('Duplicate keys in a bound array are not allowed.')
|
|
482
|
+
throw new Error('Duplicate keys in a bound array are not allowed.')
|
|
430
483
|
}
|
|
431
484
|
keys[key] = i
|
|
432
485
|
keysArr[i] = key
|
|
@@ -510,12 +563,21 @@ const evaluateElement = (element, value) => {
|
|
|
510
563
|
* Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes
|
|
511
564
|
*/
|
|
512
565
|
export const renderNode = (node) => {
|
|
513
|
-
if (node &&
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
566
|
+
if (node && node.isTemplatePlaceholder) {
|
|
567
|
+
const element = h('div')
|
|
568
|
+
node(element, 'node')
|
|
569
|
+
return element
|
|
570
|
+
} else if (node && typeof node === 'object') {
|
|
571
|
+
if (typeof node.then === 'function') {
|
|
572
|
+
let temp = h('div', { style: 'display:none', class: 'fntags-promise-marker' })
|
|
573
|
+
node.then(el => {
|
|
574
|
+
temp.replaceWith(renderNode(el))
|
|
575
|
+
temp = null
|
|
576
|
+
}).catch(e => console.error('Caught failed node promise.', e))
|
|
577
|
+
return temp
|
|
578
|
+
} else {
|
|
579
|
+
return node
|
|
580
|
+
}
|
|
519
581
|
} else if (typeof node === 'function') {
|
|
520
582
|
return renderNode(node())
|
|
521
583
|
} else {
|
|
@@ -523,6 +585,9 @@ export const renderNode = (node) => {
|
|
|
523
585
|
}
|
|
524
586
|
}
|
|
525
587
|
|
|
588
|
+
/**
|
|
589
|
+
* All of these attributes must be set to an actual boolean to function correctly
|
|
590
|
+
*/
|
|
526
591
|
const booleanAttributes = {
|
|
527
592
|
allowfullscreen: true,
|
|
528
593
|
allowpaymentrequest: true,
|
|
@@ -552,22 +617,37 @@ const booleanAttributes = {
|
|
|
552
617
|
}
|
|
553
618
|
|
|
554
619
|
const setAttribute = function (attrName, attr, element) {
|
|
555
|
-
if (
|
|
620
|
+
if (typeof attr === 'function') {
|
|
621
|
+
if (attr.isBoundAttribute) {
|
|
622
|
+
attr.init(attrName, element)
|
|
623
|
+
attr = attr()
|
|
624
|
+
} else if (attr.isTemplatePlaceholder) {
|
|
625
|
+
attr(element, 'attr', attrName)
|
|
626
|
+
return
|
|
627
|
+
} else if (attrName.startsWith('on')) {
|
|
628
|
+
element.addEventListener(attrName.substring(2), attr)
|
|
629
|
+
return
|
|
630
|
+
} else {
|
|
631
|
+
attr = attr()
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (attrName === 'style' && typeof attr === 'object') {
|
|
635
|
+
for (const style in attr) {
|
|
636
|
+
setStyle(style, attr[style], element)
|
|
637
|
+
}
|
|
638
|
+
} else if (attrName === 'class') {
|
|
639
|
+
//special handling for class to ensure the selector classes from fntemplate don't get overwritten
|
|
640
|
+
if(element.selector && element.className) {
|
|
641
|
+
element.className += ` ${attr}`
|
|
642
|
+
} else {
|
|
643
|
+
element.className = attr
|
|
644
|
+
}
|
|
645
|
+
} else if (attrName === 'value') {
|
|
556
646
|
element.setAttribute('value', attr)
|
|
557
647
|
// html5 nodes like range don't update unless the value property on the object is set
|
|
558
648
|
element.value = attr
|
|
559
649
|
} else if (booleanAttributes[attrName]) {
|
|
560
650
|
element[attrName] = !!attr
|
|
561
|
-
} else if (attrName === 'style' && typeof attr === 'object') {
|
|
562
|
-
for (const style in attr) {
|
|
563
|
-
if (typeof attr[style] === 'function' && attr[style].isBoundStyle) {
|
|
564
|
-
attr[style].init(style, element)
|
|
565
|
-
attr[style] = attr[style]()
|
|
566
|
-
}
|
|
567
|
-
element.style[style] = attr[style] && attr[style].toString()
|
|
568
|
-
}
|
|
569
|
-
} else if (typeof attr === 'function' && attrName.startsWith('on')) {
|
|
570
|
-
element.addEventListener(attrName.substring(2), attr)
|
|
571
651
|
} else {
|
|
572
652
|
if (attrName.startsWith('ns=')) {
|
|
573
653
|
element.setAttributeNS(...(attrName.slice(3).split('|')), attr)
|
|
@@ -577,18 +657,27 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
577
657
|
}
|
|
578
658
|
}
|
|
579
659
|
|
|
580
|
-
|
|
660
|
+
const setStyle = (style, styleValue, element) => {
|
|
661
|
+
if (typeof styleValue === 'function') {
|
|
662
|
+
if (styleValue.isBoundStyle) {
|
|
663
|
+
styleValue.init(style, element)
|
|
664
|
+
styleValue = styleValue()
|
|
665
|
+
} else if (styleValue.isTemplatePlaceholder) {
|
|
666
|
+
styleValue(element, 'style', style)
|
|
667
|
+
return
|
|
668
|
+
} else {
|
|
669
|
+
styleValue = styleValue()
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
element.style[style] = styleValue && styleValue.toString()
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export const isAttrs = (val) => val && typeof val === 'object' && val.nodeType === undefined && !Array.isArray(val) && typeof val.then !== 'function'
|
|
581
676
|
/**
|
|
582
677
|
* helper to get the attr object
|
|
583
678
|
*/
|
|
584
679
|
export const getAttrs = (children) => Array.isArray(children) && isAttrs(children[0]) ? children[0] : {}
|
|
585
680
|
|
|
586
|
-
/**
|
|
587
|
-
* A hidden div node to mark your place in the dom
|
|
588
|
-
* @returns {HTMLDivElement}
|
|
589
|
-
*/
|
|
590
|
-
const marker = (attrs) => h('div', Object.assign(attrs || {}, { style: 'display:none' }))
|
|
591
|
-
|
|
592
681
|
/**
|
|
593
682
|
* A function to create an element with a pre-defined style.
|
|
594
683
|
* For example, the flex* elements in fnelements.
|