@srfnstack/fntags 0.2.1 → 0.3.3
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 +149 -52
package/package.json
CHANGED
package/src/fntags.mjs
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A function to create dom elements with the given attributes and children.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* The first element of the children array can be an object containing element attributes.
|
|
5
|
+
* The attribute names are the standard attribute names used in html, and should all be lower case as usual.
|
|
6
|
+
*
|
|
7
|
+
* Any attribute starting with 'on' that is a function is added as an event listener with the 'on' removed.
|
|
8
|
+
* i.e. { onclick: fn } gets added to the element as element.addEventListener('click', fn)
|
|
9
|
+
*
|
|
10
|
+
* The style attribute can be an object and the properties of the object will be added as style properties to the element.
|
|
11
|
+
* i.e. { style: { color: blue } } becomes element.style.color = blue
|
|
6
12
|
*
|
|
7
13
|
* The rest of the arguments will be considered children of this element and appended to it in the same order as passed.
|
|
8
14
|
*
|
|
9
15
|
* @param tag html tag to use when created the element
|
|
10
|
-
* @param children optional
|
|
16
|
+
* @param children optional attributes object and children for the element
|
|
11
17
|
* @returns HTMLElement an html element
|
|
12
18
|
*
|
|
13
19
|
*/
|
|
14
|
-
export function h () {
|
|
15
|
-
|
|
16
|
-
let firstChildIdx = 1
|
|
20
|
+
export function h (tag, ...children) {
|
|
21
|
+
let firstChildIdx = 0
|
|
17
22
|
let element
|
|
18
23
|
if (tag.startsWith('ns=')) {
|
|
19
|
-
element = document.createElementNS(...(tag.slice(3).split('|')))
|
|
24
|
+
element = document.createElementNS(...( tag.slice(3).split('|') ))
|
|
20
25
|
} else {
|
|
21
26
|
element = document.createElement(tag)
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
if (isAttrs(
|
|
25
|
-
const attrs =
|
|
29
|
+
if (isAttrs(children[firstChildIdx])) {
|
|
30
|
+
const attrs = children[firstChildIdx]
|
|
26
31
|
firstChildIdx += 1
|
|
27
32
|
for (const a in attrs) {
|
|
28
|
-
|
|
29
|
-
if (typeof attr === 'function' && attr.isBoundAttribute) {
|
|
30
|
-
attr.init(a, element)
|
|
31
|
-
attr = attr()
|
|
32
|
-
}
|
|
33
|
-
setAttribute(a, attr, element)
|
|
33
|
+
setAttribute(a, attrs[a], element)
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
for (let i = firstChildIdx; i <
|
|
37
|
-
const child =
|
|
36
|
+
for (let i = firstChildIdx; i < children.length; i++) {
|
|
37
|
+
const child = children[i]
|
|
38
38
|
if (Array.isArray(child)) {
|
|
39
39
|
for (const c of child) {
|
|
40
40
|
element.append(renderNode(c))
|
|
@@ -46,16 +46,86 @@ export function h () {
|
|
|
46
46
|
return element
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
51
|
+
* defined in the template.
|
|
52
|
+
*
|
|
53
|
+
* This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
|
|
54
|
+
* the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
|
|
55
|
+
* one by one and can speed up situations where the a similar element is created many times.
|
|
56
|
+
*
|
|
57
|
+
* You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
|
|
58
|
+
* not be updated when the state changes because they will not be bound to the cloned element.
|
|
59
|
+
* Thus, all state bindings must be passed in the context to the compiled template to work correctly.
|
|
60
|
+
* @param templateFn {function(object): Node}
|
|
61
|
+
* @return {function(*): Node}
|
|
62
|
+
*/
|
|
63
|
+
export const fntemplate = templateFn => {
|
|
64
|
+
if (typeof templateFn !== 'function') {
|
|
65
|
+
throw new Error('You must pass a function to fntemplate. The function must return an html node.')
|
|
66
|
+
}
|
|
67
|
+
const placeholders = {}
|
|
68
|
+
let id = 1
|
|
69
|
+
const initContext = prop => {
|
|
70
|
+
if (!prop || typeof prop !== 'string') {
|
|
71
|
+
throw new Error('You must pass a non empty string prop name to the context function.')
|
|
72
|
+
}
|
|
73
|
+
const placeholder = (element, type, attrOrStyle) => {
|
|
74
|
+
let selector = element.__fnselector
|
|
75
|
+
if (!selector) {
|
|
76
|
+
selector = `fntpl-${id++}`
|
|
77
|
+
element.__fnselector = selector
|
|
78
|
+
element.classList.add(selector)
|
|
79
|
+
}
|
|
80
|
+
if (!placeholders[selector]) placeholders[selector] = []
|
|
81
|
+
placeholders[selector].push({ prop, type, attrOrStyle })
|
|
82
|
+
}
|
|
83
|
+
placeholder.isTemplatePlaceholder = true
|
|
84
|
+
return placeholder
|
|
85
|
+
}
|
|
86
|
+
// The initial render is cloned to prevent invalid state bindings from changing it
|
|
87
|
+
const compiled = templateFn(initContext).cloneNode(true)
|
|
88
|
+
return ctx => {
|
|
89
|
+
const clone = compiled.cloneNode(true)
|
|
90
|
+
for (const selectorClass in placeholders) {
|
|
91
|
+
let targetElement = clone.getElementsByClassName(selectorClass)[0]
|
|
92
|
+
if (!targetElement) {
|
|
93
|
+
if (clone.classList.contains(selectorClass)) {
|
|
94
|
+
targetElement = clone
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error(`Cannot find template element for selectorClass ${selectorClass}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
targetElement.classList.remove(selectorClass)
|
|
100
|
+
for (const placeholder of placeholders[selectorClass]) {
|
|
101
|
+
switch (placeholder.type) {
|
|
102
|
+
case 'node':
|
|
103
|
+
targetElement.replaceWith(renderNode(ctx[placeholder.prop]))
|
|
104
|
+
break
|
|
105
|
+
case 'attr':
|
|
106
|
+
setAttribute(placeholder.attrOrStyle, ctx[placeholder.prop], targetElement)
|
|
107
|
+
break
|
|
108
|
+
case 'style':
|
|
109
|
+
setStyle(placeholder.attrOrStyle, ctx[placeholder.prop], targetElement)
|
|
110
|
+
break
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(`Unexpected bindType ${placeholder.type}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return clone
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
49
120
|
/**
|
|
50
121
|
* Create a state object that can be bound to.
|
|
51
122
|
* @param initialValue The initial state
|
|
52
123
|
* @param mapKey A map function to extract a key from an element in the array. Receives the array value to extract the key from.
|
|
53
124
|
* @returns function A function that can be used to get and set the state.
|
|
54
|
-
* When getting the state, you get the actual reference to the underlying value.
|
|
55
|
-
*
|
|
125
|
+
* When getting the state, you get the actual reference to the underlying value.
|
|
126
|
+
* If you perform modifications to the value, be sure to call the state function with the updated value when you're done
|
|
127
|
+
* or the changes won't be reflected correctly and binding updates won't be triggered even though the state appears to be correct.
|
|
56
128
|
*
|
|
57
|
-
* SideNote: this _could_ be implemented such that it returned a clone, however that would add a great deal of overhead, and a lot of code. Thus, the decision
|
|
58
|
-
* was made that it's up to the caller to ensure that the fnstate is called whenever there are modifications.
|
|
59
129
|
*/
|
|
60
130
|
export const fnstate = (initialValue, mapKey) => {
|
|
61
131
|
const ctx = {
|
|
@@ -66,7 +136,7 @@ export const fnstate = (initialValue, mapKey) => {
|
|
|
66
136
|
nextId: 0,
|
|
67
137
|
mapKey,
|
|
68
138
|
state (newState) {
|
|
69
|
-
if (arguments.length === 0 || (arguments.length === 1 && arguments[0] === ctx.state)) {
|
|
139
|
+
if (arguments.length === 0 || ( arguments.length === 1 && arguments[0] === ctx.state )) {
|
|
70
140
|
return ctx.currentValue
|
|
71
141
|
} else {
|
|
72
142
|
ctx.currentValue = newState
|
|
@@ -92,46 +162,39 @@ export const fnstate = (initialValue, mapKey) => {
|
|
|
92
162
|
/**
|
|
93
163
|
* Bind this state to the given element
|
|
94
164
|
*
|
|
95
|
-
* @param element The element to bind to. If not a function, an update function must be passed
|
|
96
|
-
* @param update If passed this will be executed directly when the state changes with no other intervention
|
|
97
|
-
* @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
|
|
98
|
-
*/
|
|
99
|
-
ctx.state.bindAs = (element, update) => doBindAs(ctx, element, update)
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Bind this state as it's value
|
|
103
|
-
*
|
|
165
|
+
* @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
|
|
166
|
+
* @param [update] If passed this will be executed directly when the state changes with no other intervention
|
|
104
167
|
* @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
|
|
105
168
|
*/
|
|
106
|
-
ctx.state.
|
|
169
|
+
ctx.state.bindAs = (element, update) => doBindAs(ctx, element ?? ctx.state, update)
|
|
107
170
|
|
|
108
171
|
/**
|
|
109
172
|
* Bind attribute values to state changes
|
|
110
|
-
* @param attribute A function that returns an attribute value
|
|
173
|
+
* @param [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
111
174
|
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
112
175
|
*/
|
|
113
|
-
ctx.state.bindAttr = (attribute) => doBindAttr(ctx.state, attribute)
|
|
176
|
+
ctx.state.bindAttr = (attribute) => doBindAttr(ctx.state, attribute ?? ctx.state)
|
|
114
177
|
|
|
115
178
|
/**
|
|
116
179
|
* Bind style values to state changes
|
|
117
|
-
* @param style A function that returns a style's value
|
|
180
|
+
* @param [style] A function that returns a style's value. If not passed, defaults to the state's value
|
|
118
181
|
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
119
182
|
*/
|
|
120
|
-
ctx.state.bindStyle = (style) => doBindStyle(ctx.state, style)
|
|
183
|
+
ctx.state.bindStyle = (style) => doBindStyle(ctx.state, style ?? ctx.state)
|
|
121
184
|
|
|
122
185
|
/**
|
|
123
186
|
* Bind select and deselect to an element
|
|
124
|
-
* @param element The element to bind to. If not a function, an update function must be passed
|
|
187
|
+
* @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
|
|
125
188
|
* @param update If passed this will be executed directly when the state changes with no other intervention
|
|
126
189
|
*/
|
|
127
|
-
ctx.state.bindSelect = (element, update) => doBindSelect(ctx, element, update)
|
|
190
|
+
ctx.state.bindSelect = (element, update) => doBindSelect(ctx, element ?? ctx.state, update)
|
|
128
191
|
|
|
129
192
|
/**
|
|
130
193
|
* Bind select and deselect to an attribute
|
|
131
|
-
* @param attribute A function that returns an attribute value
|
|
194
|
+
* @param [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
132
195
|
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
133
196
|
*/
|
|
134
|
-
ctx.state.bindSelectAttr = (attribute) => doBindSelectAttr(ctx, attribute)
|
|
197
|
+
ctx.state.bindSelectAttr = (attribute) => doBindSelectAttr(ctx, attribute ?? ctx.state)
|
|
135
198
|
|
|
136
199
|
/**
|
|
137
200
|
* Mark the element with the given key as selected. This causes the bound select functions to be executed.
|
|
@@ -291,7 +354,7 @@ function doSelect (ctx, key) {
|
|
|
291
354
|
|
|
292
355
|
function doBindChildren (ctx, parent, element, update) {
|
|
293
356
|
parent = renderNode(parent)
|
|
294
|
-
if (parent === undefined) {
|
|
357
|
+
if (parent === undefined || parent.nodeType === undefined) {
|
|
295
358
|
throw new Error('You must provide a parent element to bind the children to. aka Need Bukkit.')
|
|
296
359
|
}
|
|
297
360
|
if (typeof element !== 'function' && typeof update !== 'function') {
|
|
@@ -303,7 +366,7 @@ function doBindChildren (ctx, parent, element, update) {
|
|
|
303
366
|
}
|
|
304
367
|
|
|
305
368
|
if (!Array.isArray(ctx.currentValue)) {
|
|
306
|
-
|
|
369
|
+
throw new Error('You can only use bindChildren with a state that contains an array. try myState([mystate]) before calling this function.')
|
|
307
370
|
}
|
|
308
371
|
ctx.currentValue = ctx.currentValue.map(v => v.isFnState ? v : fnstate(v))
|
|
309
372
|
ctx.bindContexts.push({ element, update, parent })
|
|
@@ -513,7 +576,11 @@ const evaluateElement = (element, value) => {
|
|
|
513
576
|
* Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes
|
|
514
577
|
*/
|
|
515
578
|
export const renderNode = (node) => {
|
|
516
|
-
if (node &&
|
|
579
|
+
if (node && node.isTemplatePlaceholder) {
|
|
580
|
+
const element = h('div')
|
|
581
|
+
node(element, 'node')
|
|
582
|
+
return element
|
|
583
|
+
} else if (node && typeof node === 'object') {
|
|
517
584
|
if (typeof node.then === 'function') {
|
|
518
585
|
let temp = h('div', { style: 'display:none', class: 'fntags-promise-marker' })
|
|
519
586
|
node.then(el => {
|
|
@@ -563,13 +630,30 @@ const booleanAttributes = {
|
|
|
563
630
|
}
|
|
564
631
|
|
|
565
632
|
const setAttribute = function (attrName, attr, element) {
|
|
633
|
+
if (typeof attr === 'function') {
|
|
634
|
+
if (attr.isBoundAttribute) {
|
|
635
|
+
attr.init(attrName, element)
|
|
636
|
+
attr = attr()
|
|
637
|
+
} else if (attr.isTemplatePlaceholder) {
|
|
638
|
+
attr(element, 'attr', attrName)
|
|
639
|
+
return
|
|
640
|
+
} else if (attrName.startsWith('on')) {
|
|
641
|
+
element.addEventListener(attrName.substring(2), attr)
|
|
642
|
+
return
|
|
643
|
+
} else {
|
|
644
|
+
attr = attr()
|
|
645
|
+
}
|
|
646
|
+
}
|
|
566
647
|
if (attrName === 'style' && typeof attr === 'object') {
|
|
567
648
|
for (const style in attr) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
649
|
+
setStyle(style, attr[style], element)
|
|
650
|
+
}
|
|
651
|
+
} else if (attrName === 'class') {
|
|
652
|
+
//special handling for class to ensure the selector classes from fntemplate don't get overwritten
|
|
653
|
+
if (element.__fnselector && element.className) {
|
|
654
|
+
element.className += ` ${attr}`
|
|
655
|
+
} else {
|
|
656
|
+
element.className = attr
|
|
573
657
|
}
|
|
574
658
|
} else if (attrName === 'value') {
|
|
575
659
|
element.setAttribute('value', attr)
|
|
@@ -577,17 +661,30 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
577
661
|
element.value = attr
|
|
578
662
|
} else if (booleanAttributes[attrName]) {
|
|
579
663
|
element[attrName] = !!attr
|
|
580
|
-
} else if (typeof attr === 'function' && attrName.startsWith('on')) {
|
|
581
|
-
element.addEventListener(attrName.substring(2), attr)
|
|
582
664
|
} else {
|
|
583
665
|
if (attrName.startsWith('ns=')) {
|
|
584
|
-
element.setAttributeNS(...(attrName.slice(3).split('|')), attr)
|
|
666
|
+
element.setAttributeNS(...( attrName.slice(3).split('|') ), attr)
|
|
585
667
|
} else {
|
|
586
668
|
element.setAttribute(attrName, attr)
|
|
587
669
|
}
|
|
588
670
|
}
|
|
589
671
|
}
|
|
590
672
|
|
|
673
|
+
const setStyle = (style, styleValue, element) => {
|
|
674
|
+
if (typeof styleValue === 'function') {
|
|
675
|
+
if (styleValue.isBoundStyle) {
|
|
676
|
+
styleValue.init(style, element)
|
|
677
|
+
styleValue = styleValue()
|
|
678
|
+
} else if (styleValue.isTemplatePlaceholder) {
|
|
679
|
+
styleValue(element, 'style', style)
|
|
680
|
+
return
|
|
681
|
+
} else {
|
|
682
|
+
styleValue = styleValue()
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
element.style[style] = styleValue && styleValue.toString()
|
|
686
|
+
}
|
|
687
|
+
|
|
591
688
|
export const isAttrs = (val) => val && typeof val === 'object' && val.nodeType === undefined && !Array.isArray(val) && typeof val.then !== 'function'
|
|
592
689
|
/**
|
|
593
690
|
* helper to get the attr object
|