@srfnstack/fntags 0.6.0 → 1.1.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 +18 -7
- 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 +30 -35
- package/src/fntags.d.mts.map +1 -1
- package/src/fntags.mjs +221 -176
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": "
|
|
3
|
+
"version": "1.1.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
|
-
"lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test
|
|
46
|
-
"lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test
|
|
55
|
+
"lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test",
|
|
56
|
+
"lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test",
|
|
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.
|
|
@@ -64,7 +47,6 @@ export function fntemplate(templateFn: (any: any) => Node): (any: any) => Node;
|
|
|
64
47
|
* will not be reflected correctly.
|
|
65
48
|
* @property {(path: string, value: any, fillWithObjects: boolean)=>void} setPath Set a value at the given property path
|
|
66
49
|
* @property {(subscriber: (newState: T, oldState: T)=>void) => void} subscribe Register a callback that will be executed whenever the state is changed
|
|
67
|
-
* @property {(reinit: boolean)=>{}} reset Remove all of the observers and optionally reset the value to it's initial value
|
|
68
50
|
* @property {boolean} isFnState A flag to indicate that this is a fnstate object
|
|
69
51
|
*/
|
|
70
52
|
/**
|
|
@@ -113,15 +95,32 @@ export function getAttrs(children: any): object;
|
|
|
113
95
|
* @return {T} The styled element
|
|
114
96
|
*/
|
|
115
97
|
export function styled<T extends HTMLElement | SVGElement>(style: object | string, tag: string, children: object[] | Node[]): T;
|
|
98
|
+
/**
|
|
99
|
+
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
100
|
+
* defined in the template.
|
|
101
|
+
*
|
|
102
|
+
* This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
|
|
103
|
+
* the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
|
|
104
|
+
* one by one and can speed up situations where a similar element is created many times.
|
|
105
|
+
*
|
|
106
|
+
* You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
|
|
107
|
+
* not be updated when the state changes because they will not be bound to the cloned element.
|
|
108
|
+
* All state bindings must be passed in the context to the compiled template to work correctly.
|
|
109
|
+
*
|
|
110
|
+
* @param {(any)=>Node} templateFn A function that returns a html node.
|
|
111
|
+
* @return {(any)=>Node} A function that takes a context object and returns a rendered node.
|
|
112
|
+
*
|
|
113
|
+
*/
|
|
114
|
+
export function fntemplate(templateFn: (any: any) => Node): (any: any) => Node;
|
|
116
115
|
/**
|
|
117
116
|
* A container for a state value that can be bound to.
|
|
118
117
|
*/
|
|
119
118
|
export type FnStateObj<T> = {
|
|
120
119
|
/**
|
|
121
|
-
* Bind this state to the given element function. This causes the element to be replaced when state changes.
|
|
120
|
+
* Bind this state to the given element function. This causes the element to be replaced when the state changes.
|
|
122
121
|
* If called with no parameters, the state's value will be rendered as an element.
|
|
123
122
|
*/
|
|
124
|
-
bindAs: (element?: () => (Node | any)) => Node;
|
|
123
|
+
bindAs: (element?: (newValue: T, oldValue: T) => (Node | any)) => Node;
|
|
125
124
|
/**
|
|
126
125
|
* Bind the values of this state to the given element.
|
|
127
126
|
* Values are items/elements of an array.
|
|
@@ -136,19 +135,19 @@ export type FnStateObj<T> = {
|
|
|
136
135
|
/**
|
|
137
136
|
* Bind attribute values to state changes
|
|
138
137
|
*/
|
|
139
|
-
bindAttr: (attribute?: () => (string | any)) => any;
|
|
138
|
+
bindAttr: (attribute?: (newValue: T, oldValue: T) => (string | any)) => any;
|
|
140
139
|
/**
|
|
141
140
|
* Bind style values to state changes
|
|
142
141
|
*/
|
|
143
|
-
bindStyle: (style?: () => string) => string;
|
|
142
|
+
bindStyle: (style?: (newValue: T, oldValue: T) => string) => string;
|
|
144
143
|
/**
|
|
145
144
|
* Bind selected state to an element
|
|
146
145
|
*/
|
|
147
|
-
bindSelect: (element?: () => (Node | any)) => Node;
|
|
146
|
+
bindSelect: (element?: (selectedKey: any) => (Node | any)) => Node;
|
|
148
147
|
/**
|
|
149
148
|
* Bind selected state to an attribute
|
|
150
149
|
*/
|
|
151
|
-
bindSelectAttr: (attribute?: () => (string | any)) => any;
|
|
150
|
+
bindSelectAttr: (attribute?: (selectedKey: any) => (string | any)) => any;
|
|
152
151
|
/**
|
|
153
152
|
* Mark the element with the given key as selected
|
|
154
153
|
* where the key is identified using the mapKey function passed on creation of the fnstate.
|
|
@@ -178,10 +177,6 @@ export type FnStateObj<T> = {
|
|
|
178
177
|
* Register a callback that will be executed whenever the state is changed
|
|
179
178
|
*/
|
|
180
179
|
subscribe: (subscriber: (newState: T, oldState: T) => void) => void;
|
|
181
|
-
/**
|
|
182
|
-
* Remove all of the observers and optionally reset the value to it's initial value
|
|
183
|
-
*/
|
|
184
|
-
reset: (reinit: boolean) => {};
|
|
185
180
|
/**
|
|
186
181
|
* A flag to indicate that this is a fnstate object
|
|
187
182
|
*/
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cAoIlB;AAwWD;;;;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;AAOD;;;;;;;;;;;;;;;GAeG;AACH,qDAJkB,IAAI,iBACH,IAAI,CA+DtB;;;;;;;;;kCAzvBmC,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;;;;eACtD,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.
|
|
@@ -170,7 +97,6 @@ export function fntemplate (templateFn) {
|
|
|
170
97
|
* will not be reflected correctly.
|
|
171
98
|
* @property {(path: string, value: any, fillWithObjects: boolean)=>void} setPath Set a value at the given property path
|
|
172
99
|
* @property {(subscriber: (newState: T, oldState: T)=>void) => void} subscribe Register a callback that will be executed whenever the state is changed
|
|
173
|
-
* @property {(reinit: boolean)=>{}} reset Remove all of the observers and optionally reset the value to it's initial value
|
|
174
100
|
* @property {boolean} isFnState A flag to indicate that this is a fnstate object
|
|
175
101
|
*/
|
|
176
102
|
|
|
@@ -211,6 +137,8 @@ export function fnstate (initialValue, mapKey) {
|
|
|
211
137
|
return newState
|
|
212
138
|
}
|
|
213
139
|
}
|
|
140
|
+
// make context available to static functions to avoid declaring functions on every new state
|
|
141
|
+
ctx.state._ctx = ctx
|
|
214
142
|
|
|
215
143
|
/**
|
|
216
144
|
* Bind this state to the given element
|
|
@@ -218,7 +146,7 @@ export function fnstate (initialValue, mapKey) {
|
|
|
218
146
|
* @param {((T)=>(Node|any))?} [element] The element to bind to. If not a function, an update function must be passed. If not passed, defaults to the state's value
|
|
219
147
|
* @returns {()=>Node}
|
|
220
148
|
*/
|
|
221
|
-
ctx.state.bindAs =
|
|
149
|
+
ctx.state.bindAs = doBindAs
|
|
222
150
|
|
|
223
151
|
/**
|
|
224
152
|
* Bind the values of this state to the given element.
|
|
@@ -229,7 +157,7 @@ export function fnstate (initialValue, mapKey) {
|
|
|
229
157
|
* @param {(childState: FnState)=>(Node|any)} element A function that receives each element wrapped as a fnstate and produces an element
|
|
230
158
|
* @returns {Node}
|
|
231
159
|
*/
|
|
232
|
-
ctx.state.bindChildren =
|
|
160
|
+
ctx.state.bindChildren = doBindChildren
|
|
233
161
|
|
|
234
162
|
/**
|
|
235
163
|
* Bind to a property of an object stored in this state instead of the state itself.
|
|
@@ -239,46 +167,46 @@ export function fnstate (initialValue, mapKey) {
|
|
|
239
167
|
* @param {string} prop The object property to bind as
|
|
240
168
|
* @returns {()=>Node}
|
|
241
169
|
*/
|
|
242
|
-
ctx.state.bindProp =
|
|
170
|
+
ctx.state.bindProp = doBindProp
|
|
243
171
|
|
|
244
172
|
/**
|
|
245
173
|
* Bind attribute values to state changes
|
|
246
174
|
* @param {(()=>(string|any))?} [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
247
175
|
* @returns {()=>(string|any)} A function that calls the passed function, with some extra metadata
|
|
248
176
|
*/
|
|
249
|
-
ctx.state.bindAttr =
|
|
177
|
+
ctx.state.bindAttr = doBindAttr
|
|
250
178
|
|
|
251
179
|
/**
|
|
252
180
|
* Bind style values to state changes
|
|
253
181
|
* @param {(()=>string)?} [style] A function that returns a style's value. If not passed, defaults to the state's value
|
|
254
182
|
* @returns {()=>Node} A function that calls the passed function, with some extra metadata
|
|
255
183
|
*/
|
|
256
|
-
ctx.state.bindStyle =
|
|
184
|
+
ctx.state.bindStyle = doBindStyle
|
|
257
185
|
|
|
258
186
|
/**
|
|
259
187
|
* Bind select and deselect to an element
|
|
260
188
|
* @param {(()=>(Node|any))?} [element] The element to bind to. If not passed, defaults to the state's value
|
|
261
189
|
* @returns {()=>Node}
|
|
262
190
|
*/
|
|
263
|
-
ctx.state.bindSelect =
|
|
191
|
+
ctx.state.bindSelect = doBindSelect
|
|
264
192
|
|
|
265
193
|
/**
|
|
266
194
|
* Bind select and deselect to an attribute
|
|
267
195
|
* @param {(()=>(string|any))?} [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
|
|
268
196
|
* @returns {()=>(string|any)} A function that calls the passed function, with some extra metadata
|
|
269
197
|
*/
|
|
270
|
-
ctx.state.bindSelectAttr =
|
|
198
|
+
ctx.state.bindSelectAttr = doBindSelectAttr
|
|
271
199
|
|
|
272
200
|
/**
|
|
273
201
|
* Mark the element with the given key as selected. This causes the bound select functions to be executed.
|
|
274
202
|
*/
|
|
275
|
-
ctx.state.select =
|
|
203
|
+
ctx.state.select = doSelect
|
|
276
204
|
|
|
277
205
|
/**
|
|
278
206
|
* Get the currently selected key
|
|
279
207
|
* @returns {any}
|
|
280
208
|
*/
|
|
281
|
-
ctx.state.selected =
|
|
209
|
+
ctx.state.selected = doSelected
|
|
282
210
|
|
|
283
211
|
ctx.state.isFnState = true
|
|
284
212
|
|
|
@@ -286,7 +214,7 @@ export function fnstate (initialValue, mapKey) {
|
|
|
286
214
|
* Perform an Object.assign() on the current state using the provided update
|
|
287
215
|
* @param {T} [update]
|
|
288
216
|
*/
|
|
289
|
-
ctx.state.assign =
|
|
217
|
+
ctx.state.assign = doAssign
|
|
290
218
|
|
|
291
219
|
/**
|
|
292
220
|
* Get a value at the given property path, an error is thrown if the value is not an object
|
|
@@ -295,77 +223,32 @@ export function fnstate (initialValue, mapKey) {
|
|
|
295
223
|
* will not be reflected correctly.
|
|
296
224
|
* @param {string} [path] a json path type path that points to a property
|
|
297
225
|
*/
|
|
298
|
-
ctx.state.getPath =
|
|
299
|
-
if (typeof path !== 'string') {
|
|
300
|
-
throw new Error('Invalid path')
|
|
301
|
-
}
|
|
302
|
-
if (typeof ctx.currentValue !== 'object') {
|
|
303
|
-
throw new Error('Value is not an object')
|
|
304
|
-
}
|
|
305
|
-
return path
|
|
306
|
-
.split('.')
|
|
307
|
-
.reduce(
|
|
308
|
-
(curr, part) => {
|
|
309
|
-
if (part in curr) {
|
|
310
|
-
return curr[part]
|
|
311
|
-
} else {
|
|
312
|
-
return undefined
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
ctx.currentValue
|
|
316
|
-
)
|
|
317
|
-
}
|
|
226
|
+
ctx.state.getPath = doGetPath
|
|
318
227
|
|
|
319
228
|
/**
|
|
320
229
|
* Set a value at the given property path
|
|
321
230
|
* @param {string} path The JSON path of the value to set
|
|
322
231
|
* @param {any} value The value to set the path to
|
|
323
|
-
* @param {boolean} fillWithObjects Whether to non object values with new empty objects.
|
|
232
|
+
* @param {boolean} fillWithObjects Whether to replace non object values with new empty objects.
|
|
324
233
|
*/
|
|
325
|
-
ctx.state.setPath =
|
|
326
|
-
const s = path.split('.')
|
|
327
|
-
const parent = s
|
|
328
|
-
.slice(0, -1)
|
|
329
|
-
.reduce(
|
|
330
|
-
(current, part) => {
|
|
331
|
-
if (fillWithObjects && typeof current[part] !== 'object') {
|
|
332
|
-
current[part] = {}
|
|
333
|
-
}
|
|
334
|
-
return current[part]
|
|
335
|
-
},
|
|
336
|
-
ctx.currentValue
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
if (parent && typeof parent === 'object') {
|
|
340
|
-
parent[s.slice(-1)] = value
|
|
341
|
-
ctx.state(ctx.currentValue)
|
|
342
|
-
} else {
|
|
343
|
-
throw new Error(`No object at path ${path}`)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
234
|
+
ctx.state.setPath = doSetPath
|
|
346
235
|
|
|
347
236
|
/**
|
|
348
237
|
* Register a callback that will be executed whenever the state is changed
|
|
349
238
|
* @param {(newValue:T,oldValue:T)=>void} callback
|
|
350
239
|
* @return {()=>void} a function to stop the subscription
|
|
351
240
|
*/
|
|
352
|
-
ctx.state.subscribe =
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Remove all the observers and optionally reset the value to it's initial value
|
|
356
|
-
* @param {boolean} reInit whether to reset the state to it's initial value
|
|
357
|
-
*/
|
|
358
|
-
ctx.state.reset = (reInit) => doReset(ctx, reInit, initialValue)
|
|
241
|
+
ctx.state.subscribe = doSubscribe
|
|
359
242
|
|
|
360
243
|
return ctx.state
|
|
361
244
|
}
|
|
362
245
|
|
|
363
|
-
function doSubscribe (
|
|
246
|
+
function doSubscribe (callback) {
|
|
247
|
+
const ctx = this._ctx
|
|
364
248
|
const id = ctx.nextId++
|
|
365
|
-
|
|
249
|
+
ctx.observers.push({ id, fn: callback })
|
|
366
250
|
return () => {
|
|
367
|
-
|
|
368
|
-
list = null
|
|
251
|
+
ctx.observers.splice(ctx.observers.findIndex(l => l.id === id), 1)
|
|
369
252
|
}
|
|
370
253
|
}
|
|
371
254
|
|
|
@@ -378,10 +261,15 @@ const subscribeSelect = (ctx, callback) => {
|
|
|
378
261
|
parentCtx.selectObservers[key].push(callback)
|
|
379
262
|
}
|
|
380
263
|
|
|
381
|
-
|
|
382
|
-
|
|
264
|
+
function doBindSelectAttr (attribute) {
|
|
265
|
+
attribute = attribute ?? this
|
|
266
|
+
const ctx = this._ctx
|
|
267
|
+
const attrFn = (attribute && !attribute.isFnState && typeof attribute === 'function')
|
|
268
|
+
? (...args) => attribute(args.length > 0 ? args[0] : ctx.selected)
|
|
269
|
+
: attribute
|
|
270
|
+
const boundAttr = createBoundAttr(attrFn)
|
|
383
271
|
boundAttr.init = (attrName, element) =>
|
|
384
|
-
subscribeSelect(ctx, () => setAttribute(attrName, attribute(), element))
|
|
272
|
+
subscribeSelect(ctx, (selectedKey) => setAttribute(attrName, attribute.isFnState ? attribute() : attribute(selectedKey), element))
|
|
385
273
|
return boundAttr
|
|
386
274
|
}
|
|
387
275
|
|
|
@@ -395,42 +283,101 @@ function createBoundAttr (attr) {
|
|
|
395
283
|
return boundAttr
|
|
396
284
|
}
|
|
397
285
|
|
|
398
|
-
function doBindAttr (
|
|
286
|
+
function doBindAttr (attribute) {
|
|
287
|
+
attribute = attribute ?? this
|
|
399
288
|
const boundAttr = createBoundAttr(attribute)
|
|
400
|
-
boundAttr.init = (attrName, element) =>
|
|
289
|
+
boundAttr.init = (attrName, element) => {
|
|
290
|
+
setAttribute(attrName, attribute.isFnState ? attribute() : attribute(this()), element)
|
|
291
|
+
this.subscribe((newState, oldState) => setAttribute(attrName, attribute.isFnState ? attribute() : attribute(newState, oldState), element))
|
|
292
|
+
}
|
|
401
293
|
return boundAttr
|
|
402
294
|
}
|
|
403
295
|
|
|
404
|
-
function doBindStyle (
|
|
296
|
+
function doBindStyle (style) {
|
|
297
|
+
style = style ?? this
|
|
405
298
|
if (typeof style !== 'function') {
|
|
406
299
|
throw new Error('You must pass a function to bindStyle')
|
|
407
300
|
}
|
|
408
301
|
const boundStyle = () => style()
|
|
409
302
|
boundStyle.isBoundStyle = true
|
|
410
|
-
boundStyle.init = (styleName, element) =>
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function doReset (ctx, reInit, initialValue) {
|
|
415
|
-
ctx.observers = []
|
|
416
|
-
ctx.selectObservers = {}
|
|
417
|
-
if (reInit) {
|
|
418
|
-
ctx.currentValue = initialValue
|
|
303
|
+
boundStyle.init = (styleName, element) => {
|
|
304
|
+
element.style[styleName] = style.isFnState ? style() : style(this())
|
|
305
|
+
this.subscribe((newState, oldState) => { element.style[styleName] = style.isFnState ? style() : style(newState, oldState) })
|
|
419
306
|
}
|
|
307
|
+
return boundStyle
|
|
420
308
|
}
|
|
421
309
|
|
|
422
|
-
function doSelect (
|
|
310
|
+
function doSelect (key) {
|
|
311
|
+
const ctx = this._ctx
|
|
423
312
|
const currentSelected = ctx.selected
|
|
424
313
|
ctx.selected = key
|
|
425
314
|
if (ctx.selectObservers[currentSelected] !== undefined) {
|
|
426
|
-
for (const obs of ctx.selectObservers[currentSelected]) obs()
|
|
315
|
+
for (const obs of ctx.selectObservers[currentSelected]) obs(ctx.selected)
|
|
427
316
|
}
|
|
428
317
|
if (ctx.selectObservers[ctx.selected] !== undefined) {
|
|
429
|
-
for (const obs of ctx.selectObservers[ctx.selected]) obs()
|
|
318
|
+
for (const obs of ctx.selectObservers[ctx.selected]) obs(ctx.selected)
|
|
430
319
|
}
|
|
431
320
|
}
|
|
432
321
|
|
|
433
|
-
function
|
|
322
|
+
function doSelected () {
|
|
323
|
+
return this._ctx.selected
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function doAssign (update) {
|
|
327
|
+
return this(Object.assign(this._ctx.currentValue, update))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function doGetPath (path) {
|
|
331
|
+
const ctx = this._ctx
|
|
332
|
+
if (typeof path !== 'string') {
|
|
333
|
+
throw new Error('Invalid path')
|
|
334
|
+
}
|
|
335
|
+
if (typeof ctx.currentValue !== 'object') {
|
|
336
|
+
throw new Error('Value is not an object')
|
|
337
|
+
}
|
|
338
|
+
return path
|
|
339
|
+
.split('.')
|
|
340
|
+
.reduce(
|
|
341
|
+
(curr, part) => {
|
|
342
|
+
if (part in curr) {
|
|
343
|
+
return curr[part]
|
|
344
|
+
} else {
|
|
345
|
+
return undefined
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
ctx.currentValue
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function doSetPath (path, value, fillWithObjects = false) {
|
|
353
|
+
const ctx = this._ctx
|
|
354
|
+
const s = path.split('.')
|
|
355
|
+
const parent = s
|
|
356
|
+
.slice(0, -1)
|
|
357
|
+
.reduce(
|
|
358
|
+
(current, part) => {
|
|
359
|
+
if (fillWithObjects && typeof current[part] !== 'object') {
|
|
360
|
+
current[part] = {}
|
|
361
|
+
}
|
|
362
|
+
return current[part]
|
|
363
|
+
},
|
|
364
|
+
ctx.currentValue
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if (parent && typeof parent === 'object') {
|
|
368
|
+
parent[s.slice(-1)] = value
|
|
369
|
+
this(ctx.currentValue)
|
|
370
|
+
} else {
|
|
371
|
+
throw new Error(`No object at path ${path}`)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function doBindProp (prop) {
|
|
376
|
+
return this.bindAs((st) => st[prop])
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function doBindChildren (parent, element) {
|
|
380
|
+
const ctx = this._ctx
|
|
434
381
|
parent = renderNode(parent)
|
|
435
382
|
if (parent === undefined || parent.nodeType === undefined) {
|
|
436
383
|
throw new Error('You must provide a parent element to bind the children to. aka Need Bukkit.')
|
|
@@ -448,11 +395,11 @@ function doBindChildren (ctx, parent, element) {
|
|
|
448
395
|
}
|
|
449
396
|
ctx.currentValue = ctx.currentValue.map(v => v.isFnState ? v : fnstate(v))
|
|
450
397
|
ctx.bindContexts.push({ element, parent })
|
|
451
|
-
|
|
398
|
+
this.subscribe((_, oldState) => {
|
|
452
399
|
if (!Array.isArray(ctx.currentValue)) {
|
|
453
400
|
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.')
|
|
454
401
|
new Promise((resolve) => {
|
|
455
|
-
|
|
402
|
+
this([ctx.currentValue])
|
|
456
403
|
resolve()
|
|
457
404
|
}).catch(e => {
|
|
458
405
|
console.error('Failed to update element: ')
|
|
@@ -478,8 +425,8 @@ const doBind = function (ctx, element, handleReplace) {
|
|
|
478
425
|
return () => elCtx.current
|
|
479
426
|
}
|
|
480
427
|
|
|
481
|
-
const updateReplacer = (ctx, element, elCtx) => () => {
|
|
482
|
-
let rendered = renderNode(evaluateElement(element, ctx.currentValue))
|
|
428
|
+
const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
429
|
+
let rendered = renderNode(evaluateElement(element, ctx.currentValue, oldValue))
|
|
483
430
|
if (rendered !== undefined) {
|
|
484
431
|
if (elCtx.current.key !== undefined) {
|
|
485
432
|
rendered.current.key = elCtx.current.key
|
|
@@ -505,11 +452,17 @@ const updateReplacer = (ctx, element, elCtx) => () => {
|
|
|
505
452
|
}
|
|
506
453
|
}
|
|
507
454
|
|
|
508
|
-
|
|
509
|
-
|
|
455
|
+
function doBindSelect (element) {
|
|
456
|
+
element = element ?? this
|
|
457
|
+
const ctx = this._ctx
|
|
458
|
+
return doBind(ctx, element, (elCtx) => subscribeSelect(ctx, updateReplacer(ctx, element, elCtx)))
|
|
459
|
+
}
|
|
510
460
|
|
|
511
|
-
|
|
512
|
-
|
|
461
|
+
function doBindAs (element) {
|
|
462
|
+
const ctx = this._ctx
|
|
463
|
+
const el = element ?? this
|
|
464
|
+
return doBind(ctx, el, (elCtx) => this.subscribe(updateReplacer(ctx, el, elCtx)))
|
|
465
|
+
}
|
|
513
466
|
|
|
514
467
|
/**
|
|
515
468
|
* Reconcile the state of the current array value with the state of the bound elements
|
|
@@ -543,10 +496,27 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
543
496
|
|
|
544
497
|
const keys = {}
|
|
545
498
|
const keysArr = []
|
|
499
|
+
let oldStateMap = null
|
|
546
500
|
for (const i in ctx.currentValue) {
|
|
547
501
|
let valueState = ctx.currentValue[i]
|
|
502
|
+
// if the value is not a fnstate, we need to wrap it
|
|
548
503
|
if (valueState === null || valueState === undefined || !valueState.isFnState) {
|
|
549
|
-
|
|
504
|
+
if (oldStateMap === null) {
|
|
505
|
+
oldStateMap = oldState && oldState.reduce((acc, v) => {
|
|
506
|
+
const key = keyMapper(ctx.mapKey, v.isFnState ? v() : v)
|
|
507
|
+
acc[key] = v
|
|
508
|
+
return acc
|
|
509
|
+
}, {})
|
|
510
|
+
}
|
|
511
|
+
// check if we have an old state for this key
|
|
512
|
+
const key = keyMapper(ctx.mapKey, valueState)
|
|
513
|
+
if (oldStateMap && oldStateMap[key]) {
|
|
514
|
+
const newValue = valueState
|
|
515
|
+
valueState = ctx.currentValue[i] = oldStateMap[key]
|
|
516
|
+
valueState(newValue)
|
|
517
|
+
} else {
|
|
518
|
+
valueState = ctx.currentValue[i] = fnstate(valueState)
|
|
519
|
+
}
|
|
550
520
|
}
|
|
551
521
|
const key = keyMapper(ctx.mapKey, valueState())
|
|
552
522
|
if (keys[key]) {
|
|
@@ -623,11 +593,11 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
623
593
|
}
|
|
624
594
|
}
|
|
625
595
|
|
|
626
|
-
const evaluateElement = (element, value) => {
|
|
596
|
+
const evaluateElement = (element, value, oldValue) => {
|
|
627
597
|
if (element.isFnState) {
|
|
628
598
|
return element()
|
|
629
599
|
} else {
|
|
630
|
-
return typeof element === 'function' ? element(value) : element
|
|
600
|
+
return typeof element === 'function' ? element(value, oldValue) : element
|
|
631
601
|
}
|
|
632
602
|
}
|
|
633
603
|
|
|
@@ -709,9 +679,6 @@ const setAttribute = function (attrName, attr, element) {
|
|
|
709
679
|
for (const style in attr) {
|
|
710
680
|
setStyle(style, attr[style], element)
|
|
711
681
|
}
|
|
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
682
|
} else if (attrName === 'value') {
|
|
716
683
|
element.setAttribute('value', attr)
|
|
717
684
|
// html5 nodes like range don't update unless the value property on the object is set
|
|
@@ -792,3 +759,81 @@ const stringifyStyle = style =>
|
|
|
792
759
|
typeof style === 'string'
|
|
793
760
|
? style
|
|
794
761
|
: Object.keys(style).map(prop => `${prop}:${style[prop]}`).join(';')
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
765
|
+
* defined in the template.
|
|
766
|
+
*
|
|
767
|
+
* This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
|
|
768
|
+
* the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
|
|
769
|
+
* one by one and can speed up situations where a similar element is created many times.
|
|
770
|
+
*
|
|
771
|
+
* You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
|
|
772
|
+
* not be updated when the state changes because they will not be bound to the cloned element.
|
|
773
|
+
* All state bindings must be passed in the context to the compiled template to work correctly.
|
|
774
|
+
*
|
|
775
|
+
* @param {(any)=>Node} templateFn A function that returns a html node.
|
|
776
|
+
* @return {(any)=>Node} A function that takes a context object and returns a rendered node.
|
|
777
|
+
*
|
|
778
|
+
*/
|
|
779
|
+
export function fntemplate (templateFn) {
|
|
780
|
+
if (typeof templateFn !== 'function') {
|
|
781
|
+
throw new Error('You must pass a function to fntemplate.')
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const bindingsByPath = []
|
|
785
|
+
|
|
786
|
+
const initContext = prop => {
|
|
787
|
+
const placeholder = (element, type, attr) => {
|
|
788
|
+
if (!element._tpl_bind) element._tpl_bind = []
|
|
789
|
+
element._tpl_bind.push({ prop, type, attr })
|
|
790
|
+
}
|
|
791
|
+
placeholder.isTemplatePlaceholder = true
|
|
792
|
+
return placeholder
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const root = templateFn(initContext)
|
|
796
|
+
|
|
797
|
+
const traverse = (node, path) => {
|
|
798
|
+
if (node._tpl_bind) {
|
|
799
|
+
bindingsByPath.push({ path: [...path], binds: node._tpl_bind })
|
|
800
|
+
delete node._tpl_bind
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let child = node.firstChild
|
|
804
|
+
let i = 0
|
|
805
|
+
while (child) {
|
|
806
|
+
traverse(child, [...path, i])
|
|
807
|
+
child = child.nextSibling
|
|
808
|
+
i++
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
traverse(root, [])
|
|
813
|
+
|
|
814
|
+
return (context) => {
|
|
815
|
+
const clone = root.cloneNode(true)
|
|
816
|
+
for (let i = 0; i < bindingsByPath.length; i++) {
|
|
817
|
+
const entry = bindingsByPath[i]
|
|
818
|
+
let target = clone
|
|
819
|
+
const path = entry.path
|
|
820
|
+
for (let j = 0; j < path.length; j++) {
|
|
821
|
+
target = target.childNodes[path[j]]
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const binds = entry.binds
|
|
825
|
+
for (let j = 0; j < binds.length; j++) {
|
|
826
|
+
const b = binds[j]
|
|
827
|
+
const val = context[b.prop]
|
|
828
|
+
if (b.type === 'node') {
|
|
829
|
+
target.replaceWith(renderNode(val))
|
|
830
|
+
} else if (b.type === 'attr') {
|
|
831
|
+
setAttribute(b.attr, val, target)
|
|
832
|
+
} else if (b.type === 'style') {
|
|
833
|
+
setStyle(b.attr, val, target)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return clone
|
|
838
|
+
}
|
|
839
|
+
}
|