@srfnstack/fntags 0.3.2 → 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 +48 -35
package/package.json
CHANGED
package/src/fntags.mjs
CHANGED
|
@@ -1,35 +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
33
|
setAttribute(a, attrs[a], element)
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
|
-
for (let i = firstChildIdx; i <
|
|
32
|
-
const child =
|
|
36
|
+
for (let i = firstChildIdx; i < children.length; i++) {
|
|
37
|
+
const child = children[i]
|
|
33
38
|
if (Array.isArray(child)) {
|
|
34
39
|
for (const c of child) {
|
|
35
40
|
element.append(renderNode(c))
|
|
@@ -45,26 +50,31 @@ export function h () {
|
|
|
45
50
|
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
46
51
|
* defined in the template.
|
|
47
52
|
*
|
|
48
|
-
* This allows fast rendering by
|
|
53
|
+
* This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
|
|
49
54
|
* 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
|
|
55
|
+
* one by one and can speed up situations where the a similar element is created many times.
|
|
51
56
|
*
|
|
52
57
|
* You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
|
|
53
58
|
* 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.
|
|
59
|
+
* Thus, all state bindings must be passed in the context to the compiled template to work correctly.
|
|
56
60
|
* @param templateFn {function(object): Node}
|
|
57
61
|
* @return {function(*): Node}
|
|
58
62
|
*/
|
|
59
63
|
export const fntemplate = templateFn => {
|
|
60
|
-
|
|
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 = {}
|
|
61
68
|
let id = 1
|
|
62
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
|
+
}
|
|
63
73
|
const placeholder = (element, type, attrOrStyle) => {
|
|
64
|
-
let selector = element.
|
|
74
|
+
let selector = element.__fnselector
|
|
65
75
|
if (!selector) {
|
|
66
|
-
selector = `fntpl-${
|
|
67
|
-
element.
|
|
76
|
+
selector = `fntpl-${id++}`
|
|
77
|
+
element.__fnselector = selector
|
|
68
78
|
element.classList.add(selector)
|
|
69
79
|
}
|
|
70
80
|
if (!placeholders[selector]) placeholders[selector] = []
|
|
@@ -74,16 +84,20 @@ export const fntemplate = templateFn => {
|
|
|
74
84
|
return placeholder
|
|
75
85
|
}
|
|
76
86
|
// The initial render is cloned to prevent invalid state bindings from changing it
|
|
77
|
-
const
|
|
87
|
+
const compiled = templateFn(initContext).cloneNode(true)
|
|
78
88
|
return ctx => {
|
|
79
|
-
const clone =
|
|
89
|
+
const clone = compiled.cloneNode(true)
|
|
80
90
|
for (const selectorClass in placeholders) {
|
|
81
|
-
|
|
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
|
+
}
|
|
82
99
|
targetElement.classList.remove(selectorClass)
|
|
83
100
|
for (const placeholder of placeholders[selectorClass]) {
|
|
84
|
-
if (!ctx[placeholder.prop]) {
|
|
85
|
-
console.warn(`No value provided for template prop: ${placeholder.prop}`)
|
|
86
|
-
}
|
|
87
101
|
switch (placeholder.type) {
|
|
88
102
|
case 'node':
|
|
89
103
|
targetElement.replaceWith(renderNode(ctx[placeholder.prop]))
|
|
@@ -108,11 +122,10 @@ export const fntemplate = templateFn => {
|
|
|
108
122
|
* @param initialValue The initial state
|
|
109
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.
|
|
110
124
|
* @returns function A function that can be used to get and set the state.
|
|
111
|
-
* When getting the state, you get the actual reference to the underlying value.
|
|
112
|
-
*
|
|
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.
|
|
113
128
|
*
|
|
114
|
-
* 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
|
|
115
|
-
* was made that it's up to the caller to ensure that the fnstate is called whenever there are modifications.
|
|
116
129
|
*/
|
|
117
130
|
export const fnstate = (initialValue, mapKey) => {
|
|
118
131
|
const ctx = {
|
|
@@ -123,7 +136,7 @@ export const fnstate = (initialValue, mapKey) => {
|
|
|
123
136
|
nextId: 0,
|
|
124
137
|
mapKey,
|
|
125
138
|
state (newState) {
|
|
126
|
-
if (arguments.length === 0 || (arguments.length === 1 && arguments[0] === ctx.state)) {
|
|
139
|
+
if (arguments.length === 0 || ( arguments.length === 1 && arguments[0] === ctx.state )) {
|
|
127
140
|
return ctx.currentValue
|
|
128
141
|
} else {
|
|
129
142
|
ctx.currentValue = newState
|
|
@@ -341,7 +354,7 @@ function doSelect (ctx, key) {
|
|
|
341
354
|
|
|
342
355
|
function doBindChildren (ctx, parent, element, update) {
|
|
343
356
|
parent = renderNode(parent)
|
|
344
|
-
if (parent === undefined) {
|
|
357
|
+
if (parent === undefined || parent.nodeType === undefined) {
|
|
345
358
|
throw new Error('You must provide a parent element to bind the children to. aka Need Bukkit.')
|
|
346
359
|
}
|
|
347
360
|
if (typeof element !== 'function' && typeof update !== 'function') {
|
|
@@ -353,7 +366,7 @@ function doBindChildren (ctx, parent, element, update) {
|
|
|
353
366
|
}
|
|
354
367
|
|
|
355
368
|
if (!Array.isArray(ctx.currentValue)) {
|
|
356
|
-
|
|
369
|
+
throw new Error('You can only use bindChildren with a state that contains an array. try myState([mystate]) before calling this function.')
|
|
357
370
|
}
|
|
358
371
|
ctx.currentValue = ctx.currentValue.map(v => v.isFnState ? v : fnstate(v))
|
|
359
372
|
ctx.bindContexts.push({ element, update, parent })
|
|
@@ -637,7 +650,7 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
637
650
|
}
|
|
638
651
|
} else if (attrName === 'class') {
|
|
639
652
|
//special handling for class to ensure the selector classes from fntemplate don't get overwritten
|
|
640
|
-
if(element.
|
|
653
|
+
if (element.__fnselector && element.className) {
|
|
641
654
|
element.className += ` ${attr}`
|
|
642
655
|
} else {
|
|
643
656
|
element.className = attr
|
|
@@ -650,7 +663,7 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
650
663
|
element[attrName] = !!attr
|
|
651
664
|
} else {
|
|
652
665
|
if (attrName.startsWith('ns=')) {
|
|
653
|
-
element.setAttributeNS(...(attrName.slice(3).split('|')), attr)
|
|
666
|
+
element.setAttributeNS(...( attrName.slice(3).split('|') ), attr)
|
|
654
667
|
} else {
|
|
655
668
|
element.setAttribute(attrName, attr)
|
|
656
669
|
}
|