@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.
- package/README.md +87 -4
- package/package.json +8 -3
- package/src/fntags.mjs +26 -14
package/README.md
CHANGED
|
@@ -1,7 +1,90 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
5
|
+
---
|
|
4
6
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
+
"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": "^
|
|
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(...(
|
|
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
|
|
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
|
-
*
|
|
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 || (
|
|
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
|
-
|
|
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(...(
|
|
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
|
-
|
|
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(';')
|