@srfnstack/fntags 0.5.3 → 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 +17 -6
- package/src/fnroute.d.mts +8 -27
- package/src/fnroute.d.mts.map +1 -1
- package/src/fnroute.mjs +33 -105
- 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
|
|
44
|
-
"cypress": "cypress run --spec test/** -b chrome",
|
|
53
|
+
"test": "cp src/*.mjs docs/lib/ && npm run lint && cypress run --spec \"test/**\" --headless -b chrome",
|
|
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
|
@@ -33,25 +33,6 @@ export function route(...children: (any | Node)[]): HTMLDivElement;
|
|
|
33
33
|
* @returns {Node|(()=>Node)}
|
|
34
34
|
*/
|
|
35
35
|
export function routeSwitch(...children: (any | Node)[]): Node | (() => Node);
|
|
36
|
-
/**
|
|
37
|
-
* The main function of this library. It will load the route at the specified path and render it into the container element.
|
|
38
|
-
* @param {object} options
|
|
39
|
-
* @param {string} options.routePath The path to the root of the routes. This is used to resolve the paths of the routes.
|
|
40
|
-
* @param {object} options.attrs The attributes of the container element
|
|
41
|
-
* @param {(error: Error, newPathState: object)=>void|Node} options.onerror A function that will be called if the route fails to load. The function receives the error and the current pathState object. Should return an error to display if it's not handled.
|
|
42
|
-
* @param {(node: Node, module: object)=>Node} options.frame A function that will be called with the rendered route element and the module that was loaded. The function should return a new element to be rendered.
|
|
43
|
-
* @param {boolean} options.sendRawPath If true, the raw path will be sent to the route. Otherwise, the path will be stripped of parameter values.
|
|
44
|
-
* @param {(path: string)=>string} options.formatPath A function that will be called with the raw path before it is used to load the route. The function should return a new path.
|
|
45
|
-
* @return {HTMLElement} The container element
|
|
46
|
-
*/
|
|
47
|
-
export function modRouter({ routePath, attrs, onerror, frame, sendRawPath, formatPath }: {
|
|
48
|
-
routePath: string;
|
|
49
|
-
attrs: object;
|
|
50
|
-
onerror: (error: Error, newPathState: object) => void | Node;
|
|
51
|
-
frame: (node: Node, module: object) => Node;
|
|
52
|
-
sendRawPath: boolean;
|
|
53
|
-
formatPath: (path: string) => string;
|
|
54
|
-
}): HTMLElement;
|
|
55
36
|
/**
|
|
56
37
|
* A link element that is a link to another route in this single page app
|
|
57
38
|
* @param {(Object|Node)[]} children The attributes of the anchor element and any children
|
|
@@ -68,12 +49,12 @@ export function fnlink(...children: (any | Node)[]): HTMLAnchorElement;
|
|
|
68
49
|
export function goTo(route: string, context?: any, replace?: boolean, silent?: boolean): void;
|
|
69
50
|
/**
|
|
70
51
|
* Listen for routing events
|
|
71
|
-
* @param event a string event to listen for
|
|
52
|
+
* @param {string} event a string event to listen for
|
|
72
53
|
* @param handler A function that will be called when the event occurs.
|
|
73
54
|
* The function receives the new and old pathState objects, in that order.
|
|
74
55
|
* @return {()=>void} a function to stop listening with the passed handler.
|
|
75
56
|
*/
|
|
76
|
-
export function listenFor(event:
|
|
57
|
+
export function listenFor(event: string, handler: any): () => void;
|
|
77
58
|
/**
|
|
78
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.
|
|
79
60
|
* @param {string} rootPath The root path of the app
|
|
@@ -85,18 +66,18 @@ export function setRootPath(rootPath: string): void;
|
|
|
85
66
|
*/
|
|
86
67
|
/**
|
|
87
68
|
* The path parameters of the current route
|
|
88
|
-
* @type {import(
|
|
69
|
+
* @type {import('./fntags.mjs').FnState<PathParameters>}
|
|
89
70
|
*/
|
|
90
|
-
export const pathParameters: import(
|
|
71
|
+
export const pathParameters: import('./fntags.mjs').FnState<PathParameters>;
|
|
91
72
|
/**
|
|
92
73
|
* The path information for a route
|
|
93
|
-
* @typedef {{
|
|
74
|
+
* @typedef {{currentPath: string, rootPath: string, context: any}} PathState
|
|
94
75
|
*/
|
|
95
76
|
/**
|
|
96
77
|
* The current path state
|
|
97
|
-
* @type {import(
|
|
78
|
+
* @type {import('./fntags.mjs').FnState<PathState>}
|
|
98
79
|
*/
|
|
99
|
-
export const pathState: import(
|
|
80
|
+
export const pathState: import('./fntags.mjs').FnState<PathState>;
|
|
100
81
|
/**
|
|
101
82
|
* @typedef {string} RouteEvent
|
|
102
83
|
*/
|
|
@@ -123,7 +104,7 @@ export type PathParameters = any;
|
|
|
123
104
|
* The path information for a route
|
|
124
105
|
*/
|
|
125
106
|
export type PathState = {
|
|
126
|
-
|
|
107
|
+
currentPath: string;
|
|
127
108
|
rootPath: string;
|
|
128
109
|
context: any;
|
|
129
110
|
};
|
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,
|
|
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
|
@@ -70,6 +70,7 @@ export function routeSwitch (...children) {
|
|
|
70
70
|
if (path) {
|
|
71
71
|
const shouldDisplay = shouldDisplayRoute(path, !!child.absolute || child.getAttribute('absolute') === 'true')
|
|
72
72
|
if (shouldDisplay) {
|
|
73
|
+
routeState.currentRoute = path
|
|
73
74
|
updatePathParameters()
|
|
74
75
|
child.updateRoute(true)
|
|
75
76
|
sw.append(child)
|
|
@@ -81,105 +82,19 @@ export function routeSwitch (...children) {
|
|
|
81
82
|
)
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
function stripParameterValues (currentRoute) {
|
|
85
|
-
return removeTrailingSlash(currentRoute.substring(1)).split('/').reduce((res, part) => {
|
|
86
|
-
const paramStart = part.indexOf(':')
|
|
87
|
-
let value = part
|
|
88
|
-
if (paramStart > -1) {
|
|
89
|
-
value = part.substring(0, paramStart)
|
|
90
|
-
}
|
|
91
|
-
return `${res}/${value}`
|
|
92
|
-
}, '')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const moduleCache = {}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* The main function of this library. It will load the route at the specified path and render it into the container element.
|
|
99
|
-
* @param {object} options
|
|
100
|
-
* @param {string} options.routePath The path to the root of the routes. This is used to resolve the paths of the routes.
|
|
101
|
-
* @param {object} options.attrs The attributes of the container element
|
|
102
|
-
* @param {(error: Error, newPathState: object)=>void|Node} options.onerror A function that will be called if the route fails to load. The function receives the error and the current pathState object. Should return an error to display if it's not handled.
|
|
103
|
-
* @param {(node: Node, module: object)=>Node} options.frame A function that will be called with the rendered route element and the module that was loaded. The function should return a new element to be rendered.
|
|
104
|
-
* @param {boolean} options.sendRawPath If true, the raw path will be sent to the route. Otherwise, the path will be stripped of parameter values.
|
|
105
|
-
* @param {(path: string)=>string} options.formatPath A function that will be called with the raw path before it is used to load the route. The function should return a new path.
|
|
106
|
-
* @return {HTMLElement} The container element
|
|
107
|
-
*/
|
|
108
|
-
export function modRouter ({ routePath, attrs, onerror, frame, sendRawPath, formatPath }) {
|
|
109
|
-
const container = h('div', attrs || {})
|
|
110
|
-
if (!routePath) {
|
|
111
|
-
throw new Error('You must provide a root url for modRouter. Routes in the ui will be looked up relative to this url.')
|
|
112
|
-
}
|
|
113
|
-
const loadRoute = (newPathState) => {
|
|
114
|
-
let path = newPathState.currentRoute
|
|
115
|
-
if (!sendRawPath) {
|
|
116
|
-
path = stripParameterValues(newPathState.currentRoute)
|
|
117
|
-
}
|
|
118
|
-
if (typeof formatPath === 'function') {
|
|
119
|
-
path = formatPath(path)
|
|
120
|
-
}
|
|
121
|
-
const filePath = path ? routePath + ensureOnlyLeadingSlash(path) : routePath
|
|
122
|
-
|
|
123
|
-
const p = moduleCache[filePath]
|
|
124
|
-
? Promise.resolve(moduleCache[filePath])
|
|
125
|
-
: import(filePath).then(m => {
|
|
126
|
-
moduleCache[filePath] = m
|
|
127
|
-
return m
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
p.then(module => {
|
|
131
|
-
const route = module.default
|
|
132
|
-
if (route) {
|
|
133
|
-
while (container.firstChild) {
|
|
134
|
-
container.removeChild(container.firstChild)
|
|
135
|
-
}
|
|
136
|
-
let node = renderNode(route)
|
|
137
|
-
if (typeof frame === 'function') {
|
|
138
|
-
node = renderNode(frame(node, module))
|
|
139
|
-
}
|
|
140
|
-
if (node) {
|
|
141
|
-
container.append(node)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
.catch(err => {
|
|
146
|
-
while (container.firstChild) {
|
|
147
|
-
container.removeChild(container.firstChild)
|
|
148
|
-
}
|
|
149
|
-
if (typeof onerror === 'function') {
|
|
150
|
-
err = onerror(err, newPathState)
|
|
151
|
-
if (err) {
|
|
152
|
-
container.append(err)
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
console.error('Failed to load route: ', err)
|
|
156
|
-
container.append('Failed to load route.')
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
listenFor(afterRouteChange, loadRoute)
|
|
161
|
-
updatePathParameters()
|
|
162
|
-
loadRoute(pathState())
|
|
163
|
-
return container
|
|
164
|
-
}
|
|
165
|
-
|
|
166
85
|
function updatePathParameters () {
|
|
167
|
-
const path =
|
|
86
|
+
const path = routeState.currentRoute
|
|
87
|
+
const currentPath = pathState().currentPath
|
|
168
88
|
const pathParts = path.split('/')
|
|
89
|
+
const currentPathParts = currentPath.split('/')
|
|
169
90
|
|
|
170
91
|
const parameters = {
|
|
171
92
|
idx: []
|
|
172
93
|
}
|
|
173
94
|
for (let i = 0; i < pathParts.length; i++) {
|
|
174
95
|
const part = pathParts[i]
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const paramName = part.substring(0, paramStart)
|
|
178
|
-
const paramValue = part.substring(paramStart + 1)
|
|
179
|
-
parameters.idx.push(paramValue)
|
|
180
|
-
if (paramName) {
|
|
181
|
-
parameters[paramName] = paramValue
|
|
182
|
-
}
|
|
96
|
+
if (part.startsWith(':')) {
|
|
97
|
+
parameters[part.substring(1)] = currentPathParts[i]
|
|
183
98
|
}
|
|
184
99
|
}
|
|
185
100
|
pathParameters(parameters)
|
|
@@ -224,7 +139,7 @@ export function goTo (route, context = {}, replace = false, silent = false) {
|
|
|
224
139
|
const newPath = window.location.origin + makePath(route)
|
|
225
140
|
|
|
226
141
|
const patch = {
|
|
227
|
-
|
|
142
|
+
currentPath: route.split(/[#?]/)[0],
|
|
228
143
|
context
|
|
229
144
|
}
|
|
230
145
|
|
|
@@ -246,10 +161,9 @@ export function goTo (route, context = {}, replace = false, silent = false) {
|
|
|
246
161
|
|
|
247
162
|
setTimeout(() => {
|
|
248
163
|
pathState.assign({
|
|
249
|
-
|
|
164
|
+
currentPath: route.split(/[#?]/)[0],
|
|
250
165
|
context
|
|
251
166
|
})
|
|
252
|
-
updatePathParameters()
|
|
253
167
|
if (!silent) {
|
|
254
168
|
emit(afterRouteChange, newPathState, oldPathState)
|
|
255
169
|
}
|
|
@@ -276,26 +190,30 @@ const removeTrailingSlash = part => part.endsWith('/') && part.length > 1 ? part
|
|
|
276
190
|
|
|
277
191
|
/**
|
|
278
192
|
* The path parameters of the current route
|
|
279
|
-
* @type {import(
|
|
193
|
+
* @type {import('./fntags.mjs').FnState<PathParameters>}
|
|
280
194
|
*/
|
|
281
195
|
export const pathParameters = fnstate({})
|
|
282
196
|
|
|
283
197
|
/**
|
|
284
198
|
* The path information for a route
|
|
285
|
-
* @typedef {{
|
|
199
|
+
* @typedef {{currentPath: string, rootPath: string, context: any}} PathState
|
|
286
200
|
*/
|
|
287
201
|
|
|
288
202
|
/**
|
|
289
203
|
* The current path state
|
|
290
|
-
* @type {import(
|
|
204
|
+
* @type {import('./fntags.mjs').FnState<PathState>}
|
|
291
205
|
*/
|
|
292
206
|
export const pathState = fnstate(
|
|
293
207
|
{
|
|
294
208
|
rootPath: ensureOnlyLeadingSlash(window.location.pathname),
|
|
295
|
-
|
|
209
|
+
currentPath: ensureOnlyLeadingSlash(window.location.pathname),
|
|
296
210
|
context: null
|
|
297
211
|
})
|
|
298
212
|
|
|
213
|
+
const routeState = {
|
|
214
|
+
currentRoute: null
|
|
215
|
+
}
|
|
216
|
+
|
|
299
217
|
/**
|
|
300
218
|
* @typedef {string} RouteEvent
|
|
301
219
|
*/
|
|
@@ -326,7 +244,7 @@ const emit = (event, newPathState, oldPathState) => {
|
|
|
326
244
|
|
|
327
245
|
/**
|
|
328
246
|
* Listen for routing events
|
|
329
|
-
* @param event a string event to listen for
|
|
247
|
+
* @param {string} event a string event to listen for
|
|
330
248
|
* @param handler A function that will be called when the event occurs.
|
|
331
249
|
* The function receives the new and old pathState objects, in that order.
|
|
332
250
|
* @return {()=>void} a function to stop listening with the passed handler.
|
|
@@ -351,7 +269,7 @@ export function listenFor (event, handler) {
|
|
|
351
269
|
export function setRootPath (rootPath) {
|
|
352
270
|
return pathState.assign({
|
|
353
271
|
rootPath: ensureOnlyLeadingSlash(rootPath),
|
|
354
|
-
|
|
272
|
+
currentPath: ensureOnlyLeadingSlash(window.location.pathname.replace(new RegExp('^' + rootPath), '')) || '/'
|
|
355
273
|
})
|
|
356
274
|
}
|
|
357
275
|
|
|
@@ -360,18 +278,17 @@ window.addEventListener(
|
|
|
360
278
|
() => {
|
|
361
279
|
const oldPathState = pathState()
|
|
362
280
|
const patch = {
|
|
363
|
-
|
|
281
|
+
currentPath: ensureOnlyLeadingSlash(window.location.pathname.replace(new RegExp('^' + pathState().rootPath), '')) || '/'
|
|
364
282
|
}
|
|
365
283
|
const newPathState = Object.assign({}, oldPathState, patch)
|
|
366
284
|
try {
|
|
367
285
|
emit(beforeRouteChange, newPathState, oldPathState)
|
|
368
286
|
} catch (e) {
|
|
369
287
|
console.trace('Path change cancelled', e)
|
|
370
|
-
goTo(oldPathState.
|
|
288
|
+
goTo(oldPathState.currentPath, oldPathState.context, true, true)
|
|
371
289
|
return
|
|
372
290
|
}
|
|
373
291
|
pathState.assign(patch)
|
|
374
|
-
updatePathParameters()
|
|
375
292
|
emit(afterRouteChange, newPathState, oldPathState)
|
|
376
293
|
emit(routeChangeComplete, newPathState, oldPathState)
|
|
377
294
|
}
|
|
@@ -379,13 +296,24 @@ window.addEventListener(
|
|
|
379
296
|
|
|
380
297
|
const makePath = path => (pathState().rootPath === '/' ? '' : pathState().rootPath) + ensureOnlyLeadingSlash(path)
|
|
381
298
|
|
|
299
|
+
const makePathPattern = (path, absolute) => {
|
|
300
|
+
const pathParts = path.replace(/[?#].*/, '').replace(/\/$/, '').split('/')
|
|
301
|
+
return '^' + pathParts.map(part => {
|
|
302
|
+
if (part.startsWith(':')) {
|
|
303
|
+
return '([^/]+)'
|
|
304
|
+
} else {
|
|
305
|
+
return part
|
|
306
|
+
}
|
|
307
|
+
}).join('/') + (absolute ? '/?$' : '(/.*|$)')
|
|
308
|
+
}
|
|
309
|
+
|
|
382
310
|
const shouldDisplayRoute = (route, isAbsolute) => {
|
|
383
311
|
const path = makePath(route)
|
|
384
312
|
const currPath = window.location.pathname
|
|
313
|
+
const pattern = makePathPattern(path, isAbsolute)
|
|
385
314
|
if (isAbsolute) {
|
|
386
|
-
return currPath === path || currPath === (path + '/') || currPath.match(
|
|
315
|
+
return currPath === path || currPath === (path + '/') || currPath.match(pattern)
|
|
387
316
|
} else {
|
|
388
|
-
const pattern = path.replace(/\/\$[^/]+(\/|$)/, '/[^/]+$1').replace(/^(.*)\/([^/]*)$/, '$1/?$2([/?#]|$)')
|
|
389
317
|
return !!currPath.match(pattern)
|
|
390
318
|
}
|
|
391
319
|
}
|
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
|