@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 +87 -4
- package/package.json +8 -3
- package/src/fnroute.mjs +6 -1
- package/src/fntags.mjs +36 -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.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": "^
|
|
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]
|
|
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(...(
|
|
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
|
|
@@ -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
|
-
|
|
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(...(
|
|
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
|
-
|
|
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(';')
|