@srfnstack/fntags 0.2.1 → 0.3.0
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 +109 -32
package/package.json
CHANGED
package/src/fntags.mjs
CHANGED
|
@@ -25,12 +25,7 @@ export function h () {
|
|
|
25
25
|
const attrs = arguments[firstChildIdx]
|
|
26
26
|
firstChildIdx += 1
|
|
27
27
|
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)
|
|
28
|
+
setAttribute(a, attrs[a], element)
|
|
34
29
|
}
|
|
35
30
|
}
|
|
36
31
|
for (let i = firstChildIdx; i < arguments.length; i++) {
|
|
@@ -46,6 +41,68 @@ export function h () {
|
|
|
46
41
|
return element
|
|
47
42
|
}
|
|
48
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.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
|
+
|
|
49
106
|
/**
|
|
50
107
|
* Create a state object that can be bound to.
|
|
51
108
|
* @param initialValue The initial state
|
|
@@ -92,46 +149,39 @@ export const fnstate = (initialValue, mapKey) => {
|
|
|
92
149
|
/**
|
|
93
150
|
* Bind this state to the given element
|
|
94
151
|
*
|
|
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
|
-
*
|
|
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
|
|
104
154
|
* @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
|
|
105
155
|
*/
|
|
106
|
-
ctx.state.
|
|
156
|
+
ctx.state.bindAs = (element, update) => doBindAs(ctx, element ?? ctx.state, update)
|
|
107
157
|
|
|
108
158
|
/**
|
|
109
159
|
* Bind attribute values to state changes
|
|
110
|
-
* @param attribute A function that returns an attribute value
|
|
160
|
+
* @param [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
111
161
|
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
112
162
|
*/
|
|
113
|
-
ctx.state.bindAttr = (attribute) => doBindAttr(ctx.state, attribute)
|
|
163
|
+
ctx.state.bindAttr = (attribute) => doBindAttr(ctx.state, attribute ?? ctx.state)
|
|
114
164
|
|
|
115
165
|
/**
|
|
116
166
|
* Bind style values to state changes
|
|
117
|
-
* @param style A function that returns a style's value
|
|
167
|
+
* @param [style] A function that returns a style's value. If not passed, defaults to the state's value
|
|
118
168
|
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
119
169
|
*/
|
|
120
|
-
ctx.state.bindStyle = (style) => doBindStyle(ctx.state, style)
|
|
170
|
+
ctx.state.bindStyle = (style) => doBindStyle(ctx.state, style ?? ctx.state)
|
|
121
171
|
|
|
122
172
|
/**
|
|
123
173
|
* 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
|
|
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
|
|
125
175
|
* @param update If passed this will be executed directly when the state changes with no other intervention
|
|
126
176
|
*/
|
|
127
|
-
ctx.state.bindSelect = (element, update) => doBindSelect(ctx, element, update)
|
|
177
|
+
ctx.state.bindSelect = (element, update) => doBindSelect(ctx, element ?? ctx.state, update)
|
|
128
178
|
|
|
129
179
|
/**
|
|
130
180
|
* Bind select and deselect to an attribute
|
|
131
|
-
* @param attribute A function that returns an attribute value
|
|
181
|
+
* @param [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
132
182
|
* @returns {function(): *} A function that calls the passed function, with some extra metadata
|
|
133
183
|
*/
|
|
134
|
-
ctx.state.bindSelectAttr = (attribute) => doBindSelectAttr(ctx, attribute)
|
|
184
|
+
ctx.state.bindSelectAttr = (attribute) => doBindSelectAttr(ctx, attribute ?? ctx.state)
|
|
135
185
|
|
|
136
186
|
/**
|
|
137
187
|
* Mark the element with the given key as selected. This causes the bound select functions to be executed.
|
|
@@ -513,7 +563,11 @@ const evaluateElement = (element, value) => {
|
|
|
513
563
|
* Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes
|
|
514
564
|
*/
|
|
515
565
|
export const renderNode = (node) => {
|
|
516
|
-
if (node &&
|
|
566
|
+
if (node && node.isTemplatePlaceholder) {
|
|
567
|
+
const element = h('div')
|
|
568
|
+
node(element, 'node')
|
|
569
|
+
return element
|
|
570
|
+
} else if (node && typeof node === 'object') {
|
|
517
571
|
if (typeof node.then === 'function') {
|
|
518
572
|
let temp = h('div', { style: 'display:none', class: 'fntags-promise-marker' })
|
|
519
573
|
node.then(el => {
|
|
@@ -563,13 +617,23 @@ const booleanAttributes = {
|
|
|
563
617
|
}
|
|
564
618
|
|
|
565
619
|
const setAttribute = function (attrName, attr, element) {
|
|
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
|
+
}
|
|
566
634
|
if (attrName === 'style' && typeof attr === 'object') {
|
|
567
635
|
for (const style in attr) {
|
|
568
|
-
|
|
569
|
-
attr[style].init(style, element)
|
|
570
|
-
attr[style] = attr[style]()
|
|
571
|
-
}
|
|
572
|
-
element.style[style] = attr[style] && attr[style].toString()
|
|
636
|
+
setStyle(style, attr[style], element)
|
|
573
637
|
}
|
|
574
638
|
} else if (attrName === 'value') {
|
|
575
639
|
element.setAttribute('value', attr)
|
|
@@ -577,8 +641,6 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
577
641
|
element.value = attr
|
|
578
642
|
} else if (booleanAttributes[attrName]) {
|
|
579
643
|
element[attrName] = !!attr
|
|
580
|
-
} else if (typeof attr === 'function' && attrName.startsWith('on')) {
|
|
581
|
-
element.addEventListener(attrName.substring(2), attr)
|
|
582
644
|
} else {
|
|
583
645
|
if (attrName.startsWith('ns=')) {
|
|
584
646
|
element.setAttributeNS(...(attrName.slice(3).split('|')), attr)
|
|
@@ -588,6 +650,21 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
588
650
|
}
|
|
589
651
|
}
|
|
590
652
|
|
|
653
|
+
const setStyle = (style, styleValue, element) => {
|
|
654
|
+
if (typeof styleValue === 'function') {
|
|
655
|
+
if (styleValue.isBoundStyle) {
|
|
656
|
+
styleValue.init(style, element)
|
|
657
|
+
styleValue = styleValue()
|
|
658
|
+
} else if (styleValue.isTemplatePlaceholder) {
|
|
659
|
+
styleValue(element, 'style', style)
|
|
660
|
+
return
|
|
661
|
+
} else {
|
|
662
|
+
styleValue = styleValue()
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
element.style[style] = styleValue && styleValue.toString()
|
|
666
|
+
}
|
|
667
|
+
|
|
591
668
|
export const isAttrs = (val) => val && typeof val === 'object' && val.nodeType === undefined && !Array.isArray(val) && typeof val.then !== 'function'
|
|
592
669
|
/**
|
|
593
670
|
* helper to get the attr object
|