@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/fntags.mjs +48 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srfnstack/fntags",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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
@@ -1,35 +1,40 @@
1
1
  /**
2
2
  * A function to create dom elements with the given attributes and children.
3
- * If an argument is a non-node object it is considered an attributes object, attributes are combined with Object.assign in the order received.
4
- * All standard html attributes can be passed, as well as any other property.
5
- * Strings are added as attributes via setAttribute, functions are added as event listeners, other types are set as properties.
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 attrs and children for the element
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
- const tag = arguments[0]
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(arguments[firstChildIdx])) {
25
- const attrs = arguments[firstChildIdx]
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 < arguments.length; i++) {
32
- const child = arguments[i]
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 precreating a dom element with the entire template structure and cloning and populating
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 same elements are created many times.
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
- const placeholders = { }
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.selector
74
+ let selector = element.__fnselector
65
75
  if (!selector) {
66
- selector = `fntpl-${prop}-${id++}`
67
- element.selector = selector
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 rendered = templateFn(initContext).cloneNode(true)
87
+ const compiled = templateFn(initContext).cloneNode(true)
78
88
  return ctx => {
79
- const clone = rendered.cloneNode(true)
89
+ const clone = compiled.cloneNode(true)
80
90
  for (const selectorClass in placeholders) {
81
- const targetElement = clone.classList.contains(selectorClass) ? clone : clone.getElementsByClassName(selectorClass)[0]
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. If you perform modifications to the object, be sure to set the value
112
- * when you're done or the changes won't be reflected correctly.
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
- return ctx.state.bindAs(element, update)
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.selector && element.className) {
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
  }