@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/fntags.mjs +109 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srfnstack/fntags",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "author": "Robert Kempton <r@snow87.com>",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/srfnstack/fntags",
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
- let attr = attrs[a]
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.bindSelf = () => doBindAs(ctx, 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 && typeof node === 'object') {
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
- if (typeof attr[style] === 'function' && attr[style].isBoundStyle) {
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