@srfnstack/fntags 0.3.3 → 0.3.7

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/README.md CHANGED
@@ -1,7 +1,90 @@
1
- # fntags
1
+ <p align="center">
2
+ <img alt="fntags header" src="https://raw.githubusercontent.com/SRFNStack/fntags/master/docs/fntags_header.gif">
3
+ </p>
2
4
 
3
- > not your usual f'n framework
5
+ ---
4
6
 
5
- ### [Documentation](https://srfnstack.github.io/fntags)
7
+ ## What the f?
8
+ fntags primary goal is to make the developer experience pleasant while providing high performance and neato features.
6
9
 
7
- Check the docs to get started.
10
+ - No dependencies and no build tools
11
+ <br> - Import the framework directly from your favorite cdn and start building.
12
+
13
+ - Everything is javascript
14
+ <br> - There's no special templating language to learn, and you won't be writing html. <br> - This removes the template+functionality duality and helps keep you focused by removing context switching.
15
+
16
+ - Real DOM elements
17
+ <br> - Every element is a real dom element, there's no virtual dom and no wrapper objects.
18
+
19
+ - It's familiar
20
+ <br> - fntags was inspired by React, and the state management is similar to react hooks.
21
+
22
+ - [State binding is explicit and granular](https://srfnstack.github.io/fntags/state#Binding%20State)
23
+ <br> - Bind only the text of an element, an attribute, or a style. You control the binding, replace as much or as little as you want.
24
+
25
+ - State exists without components
26
+ <br> - This allows app wide states to be maintained using export/import and removes the need for complex state management like redux.
27
+
28
+ - [Dynamic routing](https://srfnstack.github.io/fntags/routing#Dynamic%20Path%20Based%20Routing%3A%20modRouter)
29
+ <br> - The modRouter only loads the files required by each route and doesn't require bundling.
30
+ <br> - Bye bye fat bundles, hello highly cacheable routes.
31
+
32
+ - [Async Rendering](https://srfnstack.github.io/fntags/components#Async%20Rendering)
33
+ <br> - fntags will resolve promises that are passed to element functions, no special handling required.
34
+
35
+ ## [Documentation](https://srfnstack.github.io/fntags)
36
+ Check out the [documentation](https://srfnstack.github.io/fntags) to learn more!
37
+
38
+ ### f'n examples
39
+ <hr>
40
+
41
+ Components are plain functions
42
+ ```javascript
43
+ import { div } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fnelements.min.mjs'
44
+
45
+ const hello = name => div('Hello ', name)
46
+
47
+ document.body.append( hello('world!') )
48
+ ```
49
+
50
+ Two-way binding is a breeze with the bind functions provided by fnstate objects.
51
+ ```javascript
52
+ import { fnstate } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fntags.min.mjs'
53
+ import { div, input, br } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fnelements.min.mjs'
54
+
55
+ export const userName = fnstate('world')
56
+ export const appColor = fnstate('MediumTurquoise')
57
+
58
+ document.body.append(
59
+ div( { style: { color: appColor.bindStyle() } },
60
+ 'Hello ', userName.bindAs(), '!'
61
+ ),
62
+ br(),
63
+ input({
64
+ value: userName.bindAttr(),
65
+ oninput: e => userName(e.target.value)
66
+ }),
67
+ br(),
68
+ input({
69
+ value: appColor.bindAttr(),
70
+ oninput: e => appColor(e.target.value)
71
+ }),
72
+ )
73
+ ```
74
+
75
+ ### Required HTML
76
+ Unfortunately browsers lack the ability to render a js file directly, and thus you still need a tiny bit of html to bootstrap your app.
77
+
78
+ Here's an example of the only html you need to write for your entire application.
79
+
80
+ Now that these two lines are there you're set to write sweet sweet es6+ and no more html.
81
+
82
+ ```html
83
+ <html><body><script type="module">
84
+ import { div } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fnelements.min.mjs'
85
+ document.body.append(div('hello world!'))
86
+ </script></body></html>
87
+ ```
88
+
89
+ ### Benchmark
90
+ Check the latest benchmark results in the widely used [JS Web Frameworks Benchmark](https://krausest.github.io/js-framework-benchmark/current.html)!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srfnstack/fntags",
3
- "version": "0.3.3",
3
+ "version": "0.3.7",
4
4
  "author": "Robert Kempton <r@snow87.com>",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/srfnstack/fntags",
@@ -30,7 +30,8 @@
30
30
  "state-management"
31
31
  ],
32
32
  "devDependencies": {
33
- "cypress": "^6.8.0",
33
+ "cypress": "^9.2.0",
34
+ "pre-commit": "^1.2.2",
34
35
  "standard": "^16.0.4"
35
36
  },
36
37
  "scripts": {
@@ -38,5 +39,9 @@
38
39
  "cypress": "cypress run --spec test/** -b chrome",
39
40
  "lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test docs",
40
41
  "lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test docs"
41
- }
42
+ },
43
+ "pre-commit": [
44
+ "lint",
45
+ "test"
46
+ ]
42
47
  }
package/src/fnroute.mjs CHANGED
@@ -104,7 +104,12 @@ export const modRouter = ({ routePath, attrs, onerror, frame, sendRawPath, forma
104
104
  }
105
105
  const filePath = path ? routePath + ensureOnlyLeadingSlash(path) : routePath
106
106
 
107
- const p = moduleCache[filePath] ? Promise.resolve(moduleCache[filePath]) : import(filePath).then(m => { moduleCache[filePath] = m })
107
+ const p = moduleCache[filePath]
108
+ ? Promise.resolve(moduleCache[filePath])
109
+ : import(filePath).then(m => {
110
+ moduleCache[filePath] = m
111
+ return m
112
+ })
108
113
 
109
114
  p.then(module => {
110
115
  const route = module.default
package/src/fntags.mjs CHANGED
@@ -21,7 +21,7 @@ export function h (tag, ...children) {
21
21
  let firstChildIdx = 0
22
22
  let element
23
23
  if (tag.startsWith('ns=')) {
24
- element = document.createElementNS(...( tag.slice(3).split('|') ))
24
+ element = document.createElementNS(...(tag.slice(3).split('|')))
25
25
  } else {
26
26
  element = document.createElement(tag)
27
27
  }
@@ -52,11 +52,11 @@ export function h (tag, ...children) {
52
52
  *
53
53
  * This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
54
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.
55
+ * one by one and can speed up situations where a similar element is created many times.
56
56
  *
57
57
  * You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
58
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.
59
+ * All state bindings must be passed in the context to the compiled template to work correctly.
60
60
  * @param templateFn {function(object): Node}
61
61
  * @return {function(*): Node}
62
62
  */
@@ -136,12 +136,13 @@ export const fnstate = (initialValue, mapKey) => {
136
136
  nextId: 0,
137
137
  mapKey,
138
138
  state (newState) {
139
- if (arguments.length === 0 || ( arguments.length === 1 && arguments[0] === ctx.state )) {
139
+ if (arguments.length === 0 || (arguments.length === 1 && arguments[0] === ctx.state)) {
140
140
  return ctx.currentValue
141
141
  } else {
142
+ const oldState = ctx.currentValue
142
143
  ctx.currentValue = newState
143
144
  for (const observer of ctx.observers) {
144
- observer.fn(newState)
145
+ observer.fn(newState, oldState)
145
146
  }
146
147
  }
147
148
  return newState
@@ -168,6 +169,16 @@ export const fnstate = (initialValue, mapKey) => {
168
169
  */
169
170
  ctx.state.bindAs = (element, update) => doBindAs(ctx, element ?? ctx.state, update)
170
171
 
172
+ /**
173
+ * Bind a property of an object stored in this state as a simple value.
174
+ *
175
+ * Shortcut for `mystate.bindAs((current)=> current[prop])`
176
+ *
177
+ * @param {string} prop The object property to bind as
178
+ * @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
179
+ */
180
+ ctx.state.bindProp = (prop) => doBindAs(ctx, (st) => st[prop])
181
+
171
182
  /**
172
183
  * Bind attribute values to state changes
173
184
  * @param [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
@@ -312,6 +323,7 @@ function createBoundAttr (attr) {
312
323
  if (typeof attr !== 'function') {
313
324
  throw new Error('You must pass a function to bindAttr')
314
325
  }
326
+ // wrap the function to avoid modifying it
315
327
  const boundAttr = () => attr()
316
328
  boundAttr.isBoundAttribute = true
317
329
  return boundAttr
@@ -370,7 +382,7 @@ function doBindChildren (ctx, parent, element, update) {
370
382
  }
371
383
  ctx.currentValue = ctx.currentValue.map(v => v.isFnState ? v : fnstate(v))
372
384
  ctx.bindContexts.push({ element, update, parent })
373
- ctx.state.subscribe(() => {
385
+ ctx.state.subscribe((newState, oldState) => {
374
386
  if (!Array.isArray(ctx.currentValue)) {
375
387
  console.warn('A state used with bindChildren was updated to a non array value. This will be converted to an array of 1 and the state will be updated.')
376
388
  new Promise((resolve) => {
@@ -384,7 +396,7 @@ function doBindChildren (ctx, parent, element, update) {
384
396
  throw e
385
397
  })
386
398
  } else {
387
- reconcile(ctx)
399
+ reconcile(ctx, oldState)
388
400
  }
389
401
  })
390
402
  reconcile(ctx)
@@ -456,12 +468,12 @@ const doBindAs = (ctx, element, update) =>
456
468
  /**
457
469
  * Reconcile the state of the current array value with the state of the bound elements
458
470
  */
459
- function reconcile (ctx) {
471
+ function reconcile (ctx, oldState) {
460
472
  for (const bindContext of ctx.bindContexts) {
461
473
  if (bindContext.boundElementByKey === undefined) {
462
474
  bindContext.boundElementByKey = {}
463
475
  }
464
- arrangeElements(ctx, bindContext)
476
+ arrangeElements(ctx, bindContext, oldState)
465
477
  }
466
478
  }
467
479
 
@@ -475,7 +487,7 @@ function keyMapper (mapKey, value) {
475
487
  }
476
488
  }
477
489
 
478
- function arrangeElements (ctx, bindContext) {
490
+ function arrangeElements (ctx, bindContext, oldState) {
479
491
  if (ctx.currentValue.length === 0) {
480
492
  bindContext.parent.textContent = ''
481
493
  bindContext.boundElementByKey = {}
@@ -492,7 +504,8 @@ function arrangeElements (ctx, bindContext) {
492
504
  }
493
505
  const key = keyMapper(ctx.mapKey, valueState())
494
506
  if (keys[key]) {
495
- throw new Error('Duplicate keys in a bound array are not allowed.')
507
+ if (oldState) ctx.state(oldState)
508
+ throw new Error('Duplicate keys in a bound array are not allowed, state reset to previous value.')
496
509
  }
497
510
  keys[key] = i
498
511
  keysArr[i] = key
@@ -649,7 +662,7 @@ const setAttribute = function (attrName, attr, element) {
649
662
  setStyle(style, attr[style], element)
650
663
  }
651
664
  } else if (attrName === 'class') {
652
- //special handling for class to ensure the selector classes from fntemplate don't get overwritten
665
+ // special handling for class to ensure the selector classes from fntemplate don't get overwritten
653
666
  if (element.__fnselector && element.className) {
654
667
  element.className += ` ${attr}`
655
668
  } else {
@@ -663,7 +676,7 @@ const setAttribute = function (attrName, attr, element) {
663
676
  element[attrName] = !!attr
664
677
  } else {
665
678
  if (attrName.startsWith('ns=')) {
666
- element.setAttributeNS(...( attrName.slice(3).split('|') ), attr)
679
+ element.setAttributeNS(...(attrName.slice(3).split('|')), attr)
667
680
  } else {
668
681
  element.setAttribute(attrName, attr)
669
682
  }
@@ -703,9 +716,18 @@ export const getAttrs = (children) => Array.isArray(children) && isAttrs(childre
703
716
  export const styled = (style, tag, children) => {
704
717
  const firstChild = children[0]
705
718
  if (isAttrs(firstChild)) {
706
- children[0].style = Object.assign(style, firstChild.style)
719
+ if (typeof firstChild.style === 'string') {
720
+ firstChild.style = [stringifyStyle(style), stringifyStyle(firstChild.style)].join(';')
721
+ } else {
722
+ firstChild.style = Object.assign(style, firstChild.style)
723
+ }
707
724
  } else {
708
725
  children.unshift({ style })
709
726
  }
710
727
  return h(tag, ...children)
711
728
  }
729
+
730
+ const stringifyStyle = style =>
731
+ typeof style === 'string'
732
+ ? style
733
+ : Object.keys(style).map(prop => `${prop}:${style[prop]}`).join(';')