@srfnstack/fntags 0.6.0 → 1.0.0
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 +76 -54
- package/package.json +16 -5
- package/src/fnroute.d.mts +2 -2
- package/src/fnroute.d.mts.map +1 -1
- package/src/fnroute.mjs +1 -1
- package/src/fntags.d.mts +13 -30
- package/src/fntags.d.mts.map +1 -1
- package/src/fntags.mjs +42 -94
package/README.md
CHANGED
|
@@ -4,80 +4,102 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
fntags primary goal is to make the developer experience pleasant while providing high performance and neato features.
|
|
7
|
+
# fntags
|
|
9
8
|
|
|
10
|
-
-
|
|
11
|
-
<br> - Import the framework directly from your favorite cdn and start building.
|
|
9
|
+
A lightweight, no-build ES6 framework for building fast and reactive web applications.
|
|
12
10
|
|
|
13
|
-
|
|
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.
|
|
11
|
+
fntags allows you to build complex, interactive web apps using standard JavaScript and HTML5. No special syntax, no build steps, and no magic.
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
<br> - Every element is a real dom element, there's no virtual dom and no wrapper objects.
|
|
13
|
+
## Why fntags?
|
|
18
14
|
|
|
19
|
-
-
|
|
20
|
-
|
|
15
|
+
- **No Build Step**: Import the framework directly from a CDN or your file system. No Webpack, no Babel, no headaches.
|
|
16
|
+
- **Granular State**: Bind only what needs to change—text, attributes, or styles—for high-performance updates.
|
|
17
|
+
- **Standards Based**: Just standard ES6 JavaScript and HTML5. Zero magic syntax to learn.
|
|
18
|
+
- **Effortless Debugging**: In fntags, there is no black box. Errors produce clean stack traces that point exactly to your source code.
|
|
19
|
+
- **TypeScript Support**: Includes TypeScript definitions out of the box. No need to install separate `@types` packages.
|
|
20
|
+
- **Real DOM Elements**: Every element is a real DOM element. No virtual DOM and no wrapper objects.
|
|
21
|
+
- **Dynamic Routing**: Built-in path-based routing that only loads files required by each route.
|
|
21
22
|
|
|
22
|
-
|
|
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.
|
|
23
|
+
## Documentation
|
|
24
24
|
|
|
25
|
-
|
|
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.
|
|
25
|
+
Check out the [full documentation](https://srfnstack.github.io/fntags) to learn more!
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
<br> - fntags will resolve promises that are passed to element functions, no special handling required.
|
|
27
|
+
## Getting Started
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
Check out the [documentation](https://srfnstack.github.io/fntags) to learn more!
|
|
29
|
+
### Option 1: CDN (Recommended for prototyping)
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
<hr>
|
|
31
|
+
You can use fntags directly in your browser without downloading anything:
|
|
40
32
|
|
|
41
|
-
Start a new app with one file
|
|
42
33
|
```html
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
<!DOCTYPE html>
|
|
35
|
+
<html lang="en">
|
|
36
|
+
<body>
|
|
37
|
+
<script type="module">
|
|
38
|
+
import { div } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@1.0.0/src/fnelements.mjs'
|
|
39
|
+
|
|
40
|
+
document.body.append(
|
|
41
|
+
div("Hello, World!")
|
|
42
|
+
)
|
|
43
|
+
</script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
50
46
|
```
|
|
51
47
|
|
|
52
|
-
|
|
48
|
+
### Option 2: NPM
|
|
49
|
+
|
|
50
|
+
Install via npm:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install @srfnstack/fntags
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then import it in your code:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
import { div } from '@srfnstack/fntags'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
### Re-usable Components
|
|
65
|
+
|
|
66
|
+
Components in fntags are just functions that return HTML elements.
|
|
67
|
+
|
|
53
68
|
```javascript
|
|
54
|
-
|
|
69
|
+
import { div, b } from '@srfnstack/fntags'
|
|
70
|
+
|
|
71
|
+
// A simple component
|
|
72
|
+
const Greeting = (name) => {
|
|
73
|
+
return div( "Hello, ", b(name), "!")
|
|
74
|
+
}
|
|
55
75
|
|
|
56
|
-
document.body.append(
|
|
76
|
+
document.body.append(
|
|
77
|
+
Greeting("Developer")
|
|
78
|
+
)
|
|
57
79
|
```
|
|
58
80
|
|
|
59
|
-
Explicit
|
|
81
|
+
### Explicit State Binding
|
|
82
|
+
|
|
83
|
+
State binding is explicit and granular. You control exactly what updates.
|
|
84
|
+
|
|
60
85
|
```javascript
|
|
61
|
-
import { fnstate } from '
|
|
62
|
-
import { div,
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
div('Hello ', name.bindAs(), '!'),
|
|
74
|
-
br(),
|
|
75
|
-
nameInput
|
|
76
|
-
)
|
|
86
|
+
import { fnstate } from '@srfnstack/fntags'
|
|
87
|
+
import { div, button } from '@srfnstack/fntags'
|
|
88
|
+
|
|
89
|
+
const Counter = () => {
|
|
90
|
+
const count = fnstate(0)
|
|
91
|
+
|
|
92
|
+
return div(
|
|
93
|
+
div('Count: ', count.bindAs()),
|
|
94
|
+
button({
|
|
95
|
+
onclick: () => count(count() + 1)
|
|
96
|
+
}, 'Increment')
|
|
97
|
+
)
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
document.body.append(
|
|
100
|
+
document.body.append(Counter())
|
|
80
101
|
```
|
|
81
102
|
|
|
82
103
|
### Benchmark
|
|
83
|
-
|
|
104
|
+
|
|
105
|
+
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
|
+
"version": "1.0.0",
|
|
4
4
|
"author": "Robert Kempton <r@snow87.com>",
|
|
5
5
|
"private": false,
|
|
6
6
|
"homepage": "https://github.com/srfnstack/fntags",
|
|
@@ -29,7 +29,17 @@
|
|
|
29
29
|
"framework",
|
|
30
30
|
"state",
|
|
31
31
|
"two-way",
|
|
32
|
-
"state-management"
|
|
32
|
+
"state-management",
|
|
33
|
+
"router",
|
|
34
|
+
"routing",
|
|
35
|
+
"dom",
|
|
36
|
+
"ui",
|
|
37
|
+
"spa",
|
|
38
|
+
"no-build",
|
|
39
|
+
"typescript",
|
|
40
|
+
"reactive",
|
|
41
|
+
"components",
|
|
42
|
+
"no-virtual-dom"
|
|
33
43
|
],
|
|
34
44
|
"devDependencies": {
|
|
35
45
|
"cypress": "14.5.2",
|
|
@@ -40,13 +50,14 @@
|
|
|
40
50
|
"typescript": "^5.3.3"
|
|
41
51
|
},
|
|
42
52
|
"scripts": {
|
|
43
|
-
"test": "cp src/*.mjs docs/lib/ && npm run lint && cypress run --spec test
|
|
53
|
+
"test": "cp src/*.mjs docs/lib/ && npm run lint && cypress run --spec \"test/**\" --headless -b chrome",
|
|
44
54
|
"cypress": "cypress run --headed --spec test/** -b chrome",
|
|
45
55
|
"lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test docs",
|
|
46
56
|
"lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test docs",
|
|
47
57
|
"typedef": "rm -rf src/*.mts* && tsc",
|
|
48
|
-
"docs": "typedoc --plugin typedoc-plugin-markdown --out docs/types ./src/*.mjs",
|
|
49
|
-
"
|
|
58
|
+
"docs": "typedoc --plugin typedoc-plugin-markdown --out docs/types --json docs/types.json ./src/*.mjs && node scripts/generateApi.js",
|
|
59
|
+
"generate-api": "node scripts/generateApi.js",
|
|
60
|
+
"build": "npm run docs && npm run typedef && npm run lint:fix && npm run test"
|
|
50
61
|
},
|
|
51
62
|
"pre-commit": [
|
|
52
63
|
"lint",
|
package/src/fnroute.d.mts
CHANGED
|
@@ -49,12 +49,12 @@ export function fnlink(...children: (any | Node)[]): HTMLAnchorElement;
|
|
|
49
49
|
export function goTo(route: string, context?: any, replace?: boolean, silent?: boolean): void;
|
|
50
50
|
/**
|
|
51
51
|
* Listen for routing events
|
|
52
|
-
* @param event a string event to listen for
|
|
52
|
+
* @param {string} event a string event to listen for
|
|
53
53
|
* @param handler A function that will be called when the event occurs.
|
|
54
54
|
* The function receives the new and old pathState objects, in that order.
|
|
55
55
|
* @return {()=>void} a function to stop listening with the passed handler.
|
|
56
56
|
*/
|
|
57
|
-
export function listenFor(event:
|
|
57
|
+
export function listenFor(event: string, handler: any): () => void;
|
|
58
58
|
/**
|
|
59
59
|
* Set the root path of the app. This is necessary to make deep linking work in cases where the same html file is served from all paths.
|
|
60
60
|
* @param {string} rootPath The root path of the app
|
package/src/fnroute.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fnroute.d.mts","sourceRoot":"","sources":["fnroute.mjs"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,mCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,cAAc,CAoB1B;AAED;;;;;GAKG;AACH,yCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,IAAI,GAAC,CAAC,MAAI,IAAI,CAAC,CAyB3B;AAoBD;;;;GAIG;AACH,oCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,iBAAiB,CAuB7B;AAED;;;;;;GAMG;AACH,4BALW,MAAM,YACN,GAAG,YACH,OAAO,WACP,OAAO,QA4CjB;AAiED;;;;;;GAMG;AACH,
|
|
1
|
+
{"version":3,"file":"fnroute.d.mts","sourceRoot":"","sources":["fnroute.mjs"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,mCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,cAAc,CAoB1B;AAED;;;;;GAKG;AACH,yCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,IAAI,GAAC,CAAC,MAAI,IAAI,CAAC,CAyB3B;AAoBD;;;;GAIG;AACH,oCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,iBAAiB,CAuB7B;AAED;;;;;;GAMG;AACH,4BALW,MAAM,YACN,GAAG,YACH,OAAO,WACP,OAAO,QA4CjB;AAiED;;;;;;GAMG;AACH,iCALW,MAAM,iBAGL,MAAI,IAAI,CAanB;AAED;;;GAGG;AACH,sCAFW,MAAM,QAOhB;AAxFD;;;GAGG;AAEH;;;GAGG;AACH,6BAFU,OAAO,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,CAEf;AAEzC;;;GAGG;AAEH;;;GAGG;AACH,wBAFU,OAAO,cAAc,EAAE,OAAO,CAAC,SAAS,CAAC,CAO/C;AAMJ;;GAEG;AACH;;;GAGG;AACH,gCAFU,UAAU,CAEgC;AACpD;;;GAGG;AACH,+BAFU,UAAU,CAE8B;AAClD;;;GAGG;AACH,kCAFU,UAAU,CAEoC;;;;;;;;wBAnC3C;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAC;yBAmBrD,MAAM"}
|
package/src/fnroute.mjs
CHANGED
|
@@ -244,7 +244,7 @@ const emit = (event, newPathState, oldPathState) => {
|
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* Listen for routing events
|
|
247
|
-
* @param event a string event to listen for
|
|
247
|
+
* @param {string} event a string event to listen for
|
|
248
248
|
* @param handler A function that will be called when the event occurs.
|
|
249
249
|
* The function receives the new and old pathState objects, in that order.
|
|
250
250
|
* @return {()=>void} a function to stop listening with the passed handler.
|
package/src/fntags.d.mts
CHANGED
|
@@ -17,42 +17,25 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @template {HTMLElement|SVGElement} T
|
|
19
19
|
* @param {string} tag html tag to use when created the element
|
|
20
|
-
* @param {Node|Object} children optional attributes object and children for the element
|
|
20
|
+
* @param {...(Node|Object)} children optional attributes object and children for the element
|
|
21
21
|
* @return {T} an html element
|
|
22
22
|
*
|
|
23
23
|
*/
|
|
24
|
-
export function h<T extends HTMLElement | SVGElement>(tag: string, ...children: Node | any): T;
|
|
25
|
-
/**
|
|
26
|
-
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
27
|
-
* defined in the template.
|
|
28
|
-
*
|
|
29
|
-
* This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
|
|
30
|
-
* the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
|
|
31
|
-
* one by one and can speed up situations where a similar element is created many times.
|
|
32
|
-
*
|
|
33
|
-
* You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
|
|
34
|
-
* not be updated when the state changes because they will not be bound to the cloned element.
|
|
35
|
-
* All state bindings must be passed in the context to the compiled template to work correctly.
|
|
36
|
-
*
|
|
37
|
-
* @param {(any)=>Node} templateFn A function that returns a html node.
|
|
38
|
-
* @return {(any)=>Node} A function that takes a context object and returns a rendered node.
|
|
39
|
-
*
|
|
40
|
-
*/
|
|
41
|
-
export function fntemplate(templateFn: (any: any) => Node): (any: any) => Node;
|
|
24
|
+
export function h<T extends HTMLElement | SVGElement>(tag: string, ...children: (Node | any)[]): T;
|
|
42
25
|
/**
|
|
43
26
|
* @template T The type of data stored in the state container
|
|
44
27
|
* @typedef FnStateObj A container for a state value that can be bound to.
|
|
45
|
-
* @property {(element?: ()=>(Node|any))=>Node} bindAs Bind this state to the given element function. This causes the element to be replaced when state changes.
|
|
28
|
+
* @property {(element?: (newValue: T, oldValue: T)=>(Node|any))=>Node} bindAs Bind this state to the given element function. This causes the element to be replaced when the state changes.
|
|
46
29
|
* If called with no parameters, the state's value will be rendered as an element.
|
|
47
30
|
* @property {(parent: (()=>(Node|any))|any|Node, element: (childState: FnState)=>(Node|any))=>Node} bindChildren Bind the values of this state to the given element.
|
|
48
31
|
* Values are items/elements of an array.
|
|
49
32
|
* If the current value is not an array, this will behave the same as bindAs.
|
|
50
33
|
* @property {(prop: string)=>Node} bindProp Bind to a property of an object stored in this state instead of the state itself.
|
|
51
34
|
* Shortcut for `mystate.bindAs((current)=> current[prop])`
|
|
52
|
-
* @property {(attribute?: ()=>(string|any))=>any} bindAttr Bind attribute values to state changes
|
|
53
|
-
* @property {(style?: ()=>string) => string} bindStyle Bind style values to state changes
|
|
54
|
-
* @property {(element?: ()=>(Node|any))=>Node} bindSelect Bind selected state to an element
|
|
55
|
-
* @property {(attribute?: ()=>(string|any))=>any} bindSelectAttr Bind selected state to an attribute
|
|
35
|
+
* @property {(attribute?: (newValue: T, oldValue: T)=>(string|any))=>any} bindAttr Bind attribute values to state changes
|
|
36
|
+
* @property {(style?: (newValue: T, oldValue: T)=>string) => string} bindStyle Bind style values to state changes
|
|
37
|
+
* @property {(element?: (selectedKey: any)=>(Node|any))=>Node} bindSelect Bind selected state to an element
|
|
38
|
+
* @property {(attribute?: (selectedKey: any)=>(string|any))=>any} bindSelectAttr Bind selected state to an attribute
|
|
56
39
|
* @property {(key: any)=>void} select Mark the element with the given key as selected
|
|
57
40
|
* where the key is identified using the mapKey function passed on creation of the fnstate.
|
|
58
41
|
* This causes the bound select functions to be executed.
|
|
@@ -118,10 +101,10 @@ export function styled<T extends HTMLElement | SVGElement>(style: object | strin
|
|
|
118
101
|
*/
|
|
119
102
|
export type FnStateObj<T> = {
|
|
120
103
|
/**
|
|
121
|
-
* Bind this state to the given element function. This causes the element to be replaced when state changes.
|
|
104
|
+
* Bind this state to the given element function. This causes the element to be replaced when the state changes.
|
|
122
105
|
* If called with no parameters, the state's value will be rendered as an element.
|
|
123
106
|
*/
|
|
124
|
-
bindAs: (element?: () => (Node | any)) => Node;
|
|
107
|
+
bindAs: (element?: (newValue: T, oldValue: T) => (Node | any)) => Node;
|
|
125
108
|
/**
|
|
126
109
|
* Bind the values of this state to the given element.
|
|
127
110
|
* Values are items/elements of an array.
|
|
@@ -136,19 +119,19 @@ export type FnStateObj<T> = {
|
|
|
136
119
|
/**
|
|
137
120
|
* Bind attribute values to state changes
|
|
138
121
|
*/
|
|
139
|
-
bindAttr: (attribute?: () => (string | any)) => any;
|
|
122
|
+
bindAttr: (attribute?: (newValue: T, oldValue: T) => (string | any)) => any;
|
|
140
123
|
/**
|
|
141
124
|
* Bind style values to state changes
|
|
142
125
|
*/
|
|
143
|
-
bindStyle: (style?: () => string) => string;
|
|
126
|
+
bindStyle: (style?: (newValue: T, oldValue: T) => string) => string;
|
|
144
127
|
/**
|
|
145
128
|
* Bind selected state to an element
|
|
146
129
|
*/
|
|
147
|
-
bindSelect: (element?: () => (Node | any)) => Node;
|
|
130
|
+
bindSelect: (element?: (selectedKey: any) => (Node | any)) => Node;
|
|
148
131
|
/**
|
|
149
132
|
* Bind selected state to an attribute
|
|
150
133
|
*/
|
|
151
|
-
bindSelectAttr: (attribute?: () => (string | any)) => any;
|
|
134
|
+
bindSelectAttr: (attribute?: (selectedKey: any) => (string | any)) => any;
|
|
152
135
|
/**
|
|
153
136
|
* Mark the element with the given key as selected
|
|
154
137
|
* where the key is identified using the mapKey function passed on creation of the fnstate.
|
package/src/fntags.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,
|
|
1
|
+
{"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACH,CAAC,IAAI,MAAO,CAAC,OA6C1B;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cA+KlB;AAySD;;;;GAIG;AACH,iCAHW,GAAG,GACD,IAAI,CAuBhB;AAqFD;;;;GAIG;AACH,6BAHW,GAAG,GACD,OAAO,CAInB;AAED;;;;GAIG;AACH,mCAHW,GAAG,GACF,MAAM,CAIjB;AAED;;;;;;;;;;GAUG;AACH,kEALW,MAAM,GAAC,MAAM,OACb,MAAM,YACN,MAAM,EAAE,GAAC,IAAI,EAAE,KAezB;;;;;;;;;kCAnpBmC,CAAC,YAAY,CAAC,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;;2BAE/C,CAAC,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,CAAC,GAAC,GAAG,GAAC,IAAI,gCAAkC,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;qBAG9E,MAAM,KAAG,IAAI;;;;sCAEI,CAAC,YAAY,CAAC,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;mCACvC,CAAC,YAAY,CAAC,KAAG,MAAM,KAAK,MAAM;;;;yCAC7B,GAAG,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;+CACrB,GAAG,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;;;kBAC7C,GAAG,KAAG,IAAI;;;;cAGhB,MAAK,GAAG;;;;;qBACC,CAAC,KAAG,IAAI;;;;;;oBAEV,MAAM,KAAG,GAAG;;;;oBAGZ,MAAM,SAAS,GAAG,mBAAmB,OAAO,KAAG,IAAI;;;;uCAClC,CAAC,YAAY,CAAC,KAAG,IAAI,KAAK,IAAI;;;;oBAC7C,OAAO,KAAG,EAAE;;;;eACrB,OAAO;;;;;sDAKqB,CAAC,KAAG,CAAC"}
|
package/src/fntags.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @template {HTMLElement|SVGElement} T
|
|
19
19
|
* @param {string} tag html tag to use when created the element
|
|
20
|
-
* @param {Node|Object} children optional attributes object and children for the element
|
|
20
|
+
* @param {...(Node|Object)} children optional attributes object and children for the element
|
|
21
21
|
* @return {T} an html element
|
|
22
22
|
*
|
|
23
23
|
*/
|
|
@@ -72,93 +72,20 @@ function hasNs (val) {
|
|
|
72
72
|
return val.lastIndexOf(':')
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
77
|
-
* defined in the template.
|
|
78
|
-
*
|
|
79
|
-
* This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
|
|
80
|
-
* the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
|
|
81
|
-
* one by one and can speed up situations where a similar element is created many times.
|
|
82
|
-
*
|
|
83
|
-
* You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
|
|
84
|
-
* not be updated when the state changes because they will not be bound to the cloned element.
|
|
85
|
-
* All state bindings must be passed in the context to the compiled template to work correctly.
|
|
86
|
-
*
|
|
87
|
-
* @param {(any)=>Node} templateFn A function that returns a html node.
|
|
88
|
-
* @return {(any)=>Node} A function that takes a context object and returns a rendered node.
|
|
89
|
-
*
|
|
90
|
-
*/
|
|
91
|
-
export function fntemplate (templateFn) {
|
|
92
|
-
if (typeof templateFn !== 'function') {
|
|
93
|
-
throw new Error('You must pass a function to fntemplate. The function must return an html node.')
|
|
94
|
-
}
|
|
95
|
-
const placeholders = {}
|
|
96
|
-
let id = 1
|
|
97
|
-
const initContext = prop => {
|
|
98
|
-
if (!prop || typeof prop !== 'string') {
|
|
99
|
-
throw new Error('You must pass a non empty string prop name to the context function.')
|
|
100
|
-
}
|
|
101
|
-
const placeholder = (element, type, attrOrStyle) => {
|
|
102
|
-
let selector = element.__fnselector
|
|
103
|
-
if (!selector) {
|
|
104
|
-
selector = `fntpl-${id++}`
|
|
105
|
-
element.__fnselector = selector
|
|
106
|
-
element.classList.add(selector)
|
|
107
|
-
}
|
|
108
|
-
if (!placeholders[selector]) placeholders[selector] = []
|
|
109
|
-
placeholders[selector].push({ prop, type, attrOrStyle })
|
|
110
|
-
}
|
|
111
|
-
placeholder.isTemplatePlaceholder = true
|
|
112
|
-
return placeholder
|
|
113
|
-
}
|
|
114
|
-
// The initial render is cloned to prevent invalid state bindings from changing it
|
|
115
|
-
const compiled = templateFn(initContext).cloneNode(true)
|
|
116
|
-
return ctx => {
|
|
117
|
-
const clone = compiled.cloneNode(true)
|
|
118
|
-
for (const selectorClass in placeholders) {
|
|
119
|
-
let targetElement = clone.getElementsByClassName(selectorClass)[0]
|
|
120
|
-
if (!targetElement) {
|
|
121
|
-
if (clone.classList.contains(selectorClass)) {
|
|
122
|
-
targetElement = clone
|
|
123
|
-
} else {
|
|
124
|
-
throw new Error(`Cannot find template element for selectorClass ${selectorClass}`)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
targetElement.classList.remove(selectorClass)
|
|
128
|
-
for (const placeholder of placeholders[selectorClass]) {
|
|
129
|
-
switch (placeholder.type) {
|
|
130
|
-
case 'node':
|
|
131
|
-
targetElement.replaceWith(renderNode(ctx[placeholder.prop]))
|
|
132
|
-
break
|
|
133
|
-
case 'attr':
|
|
134
|
-
setAttribute(placeholder.attrOrStyle, ctx[placeholder.prop], targetElement)
|
|
135
|
-
break
|
|
136
|
-
case 'style':
|
|
137
|
-
setStyle(placeholder.attrOrStyle, ctx[placeholder.prop], targetElement)
|
|
138
|
-
break
|
|
139
|
-
default:
|
|
140
|
-
throw new Error(`Unexpected bindType ${placeholder.type}`)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return clone
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
75
|
/**
|
|
149
76
|
* @template T The type of data stored in the state container
|
|
150
77
|
* @typedef FnStateObj A container for a state value that can be bound to.
|
|
151
|
-
* @property {(element?: ()=>(Node|any))=>Node} bindAs Bind this state to the given element function. This causes the element to be replaced when state changes.
|
|
78
|
+
* @property {(element?: (newValue: T, oldValue: T)=>(Node|any))=>Node} bindAs Bind this state to the given element function. This causes the element to be replaced when the state changes.
|
|
152
79
|
* If called with no parameters, the state's value will be rendered as an element.
|
|
153
80
|
* @property {(parent: (()=>(Node|any))|any|Node, element: (childState: FnState)=>(Node|any))=>Node} bindChildren Bind the values of this state to the given element.
|
|
154
81
|
* Values are items/elements of an array.
|
|
155
82
|
* If the current value is not an array, this will behave the same as bindAs.
|
|
156
83
|
* @property {(prop: string)=>Node} bindProp Bind to a property of an object stored in this state instead of the state itself.
|
|
157
84
|
* Shortcut for `mystate.bindAs((current)=> current[prop])`
|
|
158
|
-
* @property {(attribute?: ()=>(string|any))=>any} bindAttr Bind attribute values to state changes
|
|
159
|
-
* @property {(style?: ()=>string) => string} bindStyle Bind style values to state changes
|
|
160
|
-
* @property {(element?: ()=>(Node|any))=>Node} bindSelect Bind selected state to an element
|
|
161
|
-
* @property {(attribute?: ()=>(string|any))=>any} bindSelectAttr Bind selected state to an attribute
|
|
85
|
+
* @property {(attribute?: (newValue: T, oldValue: T)=>(string|any))=>any} bindAttr Bind attribute values to state changes
|
|
86
|
+
* @property {(style?: (newValue: T, oldValue: T)=>string) => string} bindStyle Bind style values to state changes
|
|
87
|
+
* @property {(element?: (selectedKey: any)=>(Node|any))=>Node} bindSelect Bind selected state to an element
|
|
88
|
+
* @property {(attribute?: (selectedKey: any)=>(string|any))=>any} bindSelectAttr Bind selected state to an attribute
|
|
162
89
|
* @property {(key: any)=>void} select Mark the element with the given key as selected
|
|
163
90
|
* where the key is identified using the mapKey function passed on creation of the fnstate.
|
|
164
91
|
* This causes the bound select functions to be executed.
|
|
@@ -320,7 +247,7 @@ export function fnstate (initialValue, mapKey) {
|
|
|
320
247
|
* Set a value at the given property path
|
|
321
248
|
* @param {string} path The JSON path of the value to set
|
|
322
249
|
* @param {any} value The value to set the path to
|
|
323
|
-
* @param {boolean} fillWithObjects Whether to non object values with new empty objects.
|
|
250
|
+
* @param {boolean} fillWithObjects Whether to replace non object values with new empty objects.
|
|
324
251
|
*/
|
|
325
252
|
ctx.state.setPath = (path, value, fillWithObjects = false) => {
|
|
326
253
|
const s = path.split('.')
|
|
@@ -379,9 +306,12 @@ const subscribeSelect = (ctx, callback) => {
|
|
|
379
306
|
}
|
|
380
307
|
|
|
381
308
|
const doBindSelectAttr = function (ctx, attribute) {
|
|
382
|
-
const
|
|
309
|
+
const attrFn = (attribute && !attribute.isFnState && typeof attribute === 'function')
|
|
310
|
+
? (...args) => attribute(args.length > 0 ? args[0] : ctx.selected)
|
|
311
|
+
: attribute
|
|
312
|
+
const boundAttr = createBoundAttr(attrFn)
|
|
383
313
|
boundAttr.init = (attrName, element) =>
|
|
384
|
-
subscribeSelect(ctx, () => setAttribute(attrName, attribute(), element))
|
|
314
|
+
subscribeSelect(ctx, (selectedKey) => setAttribute(attrName, attribute.isFnState ? attribute() : attribute(selectedKey), element))
|
|
385
315
|
return boundAttr
|
|
386
316
|
}
|
|
387
317
|
|
|
@@ -397,7 +327,10 @@ function createBoundAttr (attr) {
|
|
|
397
327
|
|
|
398
328
|
function doBindAttr (state, attribute) {
|
|
399
329
|
const boundAttr = createBoundAttr(attribute)
|
|
400
|
-
boundAttr.init = (attrName, element) =>
|
|
330
|
+
boundAttr.init = (attrName, element) => {
|
|
331
|
+
setAttribute(attrName, attribute.isFnState ? attribute() : attribute(state()), element)
|
|
332
|
+
state.subscribe((newState, oldState) => setAttribute(attrName, attribute.isFnState ? attribute() : attribute(newState, oldState), element))
|
|
333
|
+
}
|
|
401
334
|
return boundAttr
|
|
402
335
|
}
|
|
403
336
|
|
|
@@ -407,7 +340,10 @@ function doBindStyle (state, style) {
|
|
|
407
340
|
}
|
|
408
341
|
const boundStyle = () => style()
|
|
409
342
|
boundStyle.isBoundStyle = true
|
|
410
|
-
boundStyle.init = (styleName, element) =>
|
|
343
|
+
boundStyle.init = (styleName, element) => {
|
|
344
|
+
element.style[styleName] = style.isFnState ? style() : style(state())
|
|
345
|
+
state.subscribe((newState, oldState) => { element.style[styleName] = style.isFnState ? style() : style(newState, oldState) })
|
|
346
|
+
}
|
|
411
347
|
return boundStyle
|
|
412
348
|
}
|
|
413
349
|
|
|
@@ -423,10 +359,10 @@ function doSelect (ctx, key) {
|
|
|
423
359
|
const currentSelected = ctx.selected
|
|
424
360
|
ctx.selected = key
|
|
425
361
|
if (ctx.selectObservers[currentSelected] !== undefined) {
|
|
426
|
-
for (const obs of ctx.selectObservers[currentSelected]) obs()
|
|
362
|
+
for (const obs of ctx.selectObservers[currentSelected]) obs(ctx.selected)
|
|
427
363
|
}
|
|
428
364
|
if (ctx.selectObservers[ctx.selected] !== undefined) {
|
|
429
|
-
for (const obs of ctx.selectObservers[ctx.selected]) obs()
|
|
365
|
+
for (const obs of ctx.selectObservers[ctx.selected]) obs(ctx.selected)
|
|
430
366
|
}
|
|
431
367
|
}
|
|
432
368
|
|
|
@@ -478,8 +414,8 @@ const doBind = function (ctx, element, handleReplace) {
|
|
|
478
414
|
return () => elCtx.current
|
|
479
415
|
}
|
|
480
416
|
|
|
481
|
-
const updateReplacer = (ctx, element, elCtx) => () => {
|
|
482
|
-
let rendered = renderNode(evaluateElement(element, ctx.currentValue))
|
|
417
|
+
const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
418
|
+
let rendered = renderNode(evaluateElement(element, ctx.currentValue, oldValue))
|
|
483
419
|
if (rendered !== undefined) {
|
|
484
420
|
if (elCtx.current.key !== undefined) {
|
|
485
421
|
rendered.current.key = elCtx.current.key
|
|
@@ -543,10 +479,25 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
543
479
|
|
|
544
480
|
const keys = {}
|
|
545
481
|
const keysArr = []
|
|
482
|
+
const oldStateMap = oldState && oldState.reduce((acc, v) => {
|
|
483
|
+
const key = keyMapper(ctx.mapKey, v.isFnState ? v() : v)
|
|
484
|
+
acc[key] = v
|
|
485
|
+
return acc
|
|
486
|
+
}, {})
|
|
487
|
+
|
|
546
488
|
for (const i in ctx.currentValue) {
|
|
547
489
|
let valueState = ctx.currentValue[i]
|
|
490
|
+
// if the value is not a fnstate, we need to wrap it
|
|
548
491
|
if (valueState === null || valueState === undefined || !valueState.isFnState) {
|
|
549
|
-
|
|
492
|
+
// check if we have an old state for this key
|
|
493
|
+
const key = keyMapper(ctx.mapKey, valueState)
|
|
494
|
+
if (oldStateMap && oldStateMap[key]) {
|
|
495
|
+
const newValue = valueState
|
|
496
|
+
valueState = ctx.currentValue[i] = oldStateMap[key]
|
|
497
|
+
valueState(newValue)
|
|
498
|
+
} else {
|
|
499
|
+
valueState = ctx.currentValue[i] = fnstate(valueState)
|
|
500
|
+
}
|
|
550
501
|
}
|
|
551
502
|
const key = keyMapper(ctx.mapKey, valueState())
|
|
552
503
|
if (keys[key]) {
|
|
@@ -623,11 +574,11 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
623
574
|
}
|
|
624
575
|
}
|
|
625
576
|
|
|
626
|
-
const evaluateElement = (element, value) => {
|
|
577
|
+
const evaluateElement = (element, value, oldValue) => {
|
|
627
578
|
if (element.isFnState) {
|
|
628
579
|
return element()
|
|
629
580
|
} else {
|
|
630
|
-
return typeof element === 'function' ? element(value) : element
|
|
581
|
+
return typeof element === 'function' ? element(value, oldValue) : element
|
|
631
582
|
}
|
|
632
583
|
}
|
|
633
584
|
|
|
@@ -709,9 +660,6 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
709
660
|
for (const style in attr) {
|
|
710
661
|
setStyle(style, attr[style], element)
|
|
711
662
|
}
|
|
712
|
-
} else if (element.__fnselector && element.className && attrName === 'class') {
|
|
713
|
-
// special handling for class to ensure the selector classes from fntemplate don't get overwritten
|
|
714
|
-
element.className += ` ${attr}`
|
|
715
663
|
} else if (attrName === 'value') {
|
|
716
664
|
element.setAttribute('value', attr)
|
|
717
665
|
// html5 nodes like range don't update unless the value property on the object is set
|