@srfnstack/fntags 0.3.3 → 0.3.5

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 (3) hide show
  1. package/README.md +87 -4
  2. package/package.json +8 -3
  3. package/src/fntags.mjs +26 -14
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.5",
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/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
@@ -312,6 +313,7 @@ function createBoundAttr (attr) {
312
313
  if (typeof attr !== 'function') {
313
314
  throw new Error('You must pass a function to bindAttr')
314
315
  }
316
+ // wrap the function to avoid modifying it
315
317
  const boundAttr = () => attr()
316
318
  boundAttr.isBoundAttribute = true
317
319
  return boundAttr
@@ -370,7 +372,7 @@ function doBindChildren (ctx, parent, element, update) {
370
372
  }
371
373
  ctx.currentValue = ctx.currentValue.map(v => v.isFnState ? v : fnstate(v))
372
374
  ctx.bindContexts.push({ element, update, parent })
373
- ctx.state.subscribe(() => {
375
+ ctx.state.subscribe((newState, oldState) => {
374
376
  if (!Array.isArray(ctx.currentValue)) {
375
377
  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
378
  new Promise((resolve) => {
@@ -384,7 +386,7 @@ function doBindChildren (ctx, parent, element, update) {
384
386
  throw e
385
387
  })
386
388
  } else {
387
- reconcile(ctx)
389
+ reconcile(ctx, oldState)
388
390
  }
389
391
  })
390
392
  reconcile(ctx)
@@ -456,12 +458,12 @@ const doBindAs = (ctx, element, update) =>
456
458
  /**
457
459
  * Reconcile the state of the current array value with the state of the bound elements
458
460
  */
459
- function reconcile (ctx) {
461
+ function reconcile (ctx, oldState) {
460
462
  for (const bindContext of ctx.bindContexts) {
461
463
  if (bindContext.boundElementByKey === undefined) {
462
464
  bindContext.boundElementByKey = {}
463
465
  }
464
- arrangeElements(ctx, bindContext)
466
+ arrangeElements(ctx, bindContext, oldState)
465
467
  }
466
468
  }
467
469
 
@@ -475,7 +477,7 @@ function keyMapper (mapKey, value) {
475
477
  }
476
478
  }
477
479
 
478
- function arrangeElements (ctx, bindContext) {
480
+ function arrangeElements (ctx, bindContext, oldState) {
479
481
  if (ctx.currentValue.length === 0) {
480
482
  bindContext.parent.textContent = ''
481
483
  bindContext.boundElementByKey = {}
@@ -492,7 +494,8 @@ function arrangeElements (ctx, bindContext) {
492
494
  }
493
495
  const key = keyMapper(ctx.mapKey, valueState())
494
496
  if (keys[key]) {
495
- throw new Error('Duplicate keys in a bound array are not allowed.')
497
+ if (oldState) ctx.state(oldState)
498
+ throw new Error('Duplicate keys in a bound array are not allowed, state reset to previous value.')
496
499
  }
497
500
  keys[key] = i
498
501
  keysArr[i] = key
@@ -649,7 +652,7 @@ const setAttribute = function (attrName, attr, element) {
649
652
  setStyle(style, attr[style], element)
650
653
  }
651
654
  } else if (attrName === 'class') {
652
- //special handling for class to ensure the selector classes from fntemplate don't get overwritten
655
+ // special handling for class to ensure the selector classes from fntemplate don't get overwritten
653
656
  if (element.__fnselector && element.className) {
654
657
  element.className += ` ${attr}`
655
658
  } else {
@@ -663,7 +666,7 @@ const setAttribute = function (attrName, attr, element) {
663
666
  element[attrName] = !!attr
664
667
  } else {
665
668
  if (attrName.startsWith('ns=')) {
666
- element.setAttributeNS(...( attrName.slice(3).split('|') ), attr)
669
+ element.setAttributeNS(...(attrName.slice(3).split('|')), attr)
667
670
  } else {
668
671
  element.setAttribute(attrName, attr)
669
672
  }
@@ -703,9 +706,18 @@ export const getAttrs = (children) => Array.isArray(children) && isAttrs(childre
703
706
  export const styled = (style, tag, children) => {
704
707
  const firstChild = children[0]
705
708
  if (isAttrs(firstChild)) {
706
- children[0].style = Object.assign(style, firstChild.style)
709
+ if (typeof firstChild.style === 'string') {
710
+ firstChild.style = [stringifyStyle(style), stringifyStyle(firstChild.style)].join(';')
711
+ } else {
712
+ firstChild.style = Object.assign(style, firstChild.style)
713
+ }
707
714
  } else {
708
715
  children.unshift({ style })
709
716
  }
710
717
  return h(tag, ...children)
711
718
  }
719
+
720
+ const stringifyStyle = style =>
721
+ typeof style === 'string'
722
+ ? style
723
+ : Object.keys(style).map(prop => `${prop}:${style[prop]}`).join(';')