@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 CHANGED
@@ -4,80 +4,102 @@
4
4
 
5
5
  ---
6
6
 
7
- ## What the f?
8
- fntags primary goal is to make the developer experience pleasant while providing high performance and neato features.
7
+ # fntags
9
8
 
10
- - No dependencies and no build tools
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
- - Everything is javascript
14
- <br> - There's no special templating language to learn, and you won't be writing html. <br> - This removes the template+functionality duality and helps keep you focused by removing context switching.
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
- - Real DOM elements
17
- <br> - Every element is a real dom element, there's no virtual dom and no wrapper objects.
13
+ ## Why fntags?
18
14
 
19
- - It's familiar
20
- <br> - fntags was inspired by React, and the state management is similar to react hooks.
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
- - [State binding is explicit and granular](https://srfnstack.github.io/fntags/state#Binding%20State)
23
- <br> - Bind only the text of an element, an attribute, or a style. You control the binding, replace as much or as little as you want.
23
+ ## Documentation
24
24
 
25
- - State exists without components
26
- <br> - This allows app wide states to be maintained using export/import and removes the need for complex state management like redux.
27
-
28
- - [Dynamic routing](https://srfnstack.github.io/fntags/routing#Dynamic%20Path%20Based%20Routing%3A%20modRouter)
29
- <br> - The modRouter only loads the files required by each route and doesn't require bundling.
30
- <br> - Bye bye fat bundles, hello highly cacheable routes.
25
+ Check out the [full documentation](https://srfnstack.github.io/fntags) to learn more!
31
26
 
32
- - [Async Rendering](https://srfnstack.github.io/fntags/components#Async%20Rendering)
33
- <br> - fntags will resolve promises that are passed to element functions, no special handling required.
27
+ ## Getting Started
34
28
 
35
- ## [Documentation](https://srfnstack.github.io/fntags)
36
- Check out the [documentation](https://srfnstack.github.io/fntags) to learn more!
29
+ ### Option 1: CDN (Recommended for prototyping)
37
30
 
38
- ### f'n examples
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
- <html><body>
44
- <script type="module">
45
- import { div } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.4.1/src/fnelements.min.mjs'
46
-
47
- document.body.append(div('Hello World!'))
48
- </script>
49
- </body></html>
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
- Make re-usable, customizable components using plain js functions
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
- const hello = name => div('Hello ', name)
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( hello('world!') )
76
+ document.body.append(
77
+ Greeting("Developer")
78
+ )
57
79
  ```
58
80
 
59
- Explicit two-way state binding
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 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.4.1/src/fntags.min.mjs'
62
- import { div, input, br } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.4.1/src/fnelements.min.mjs'
63
-
64
- const helloInput = () => {
65
- const name = fnstate('World')
66
-
67
- const nameInput = input({
68
- value: name.bindAttr(),
69
- oninput (){ name(nameInput.value) }
70
- })
71
-
72
- return div(
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(helloInput())
100
+ document.body.append(Counter())
80
101
  ```
81
102
 
82
103
  ### Benchmark
83
- Check the latest benchmark results in the widely used [JS Web Frameworks Benchmark](https://krausest.github.io/js-framework-benchmark/current.html)!
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.6.0",
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/** --headless -b chrome",
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 docs",
46
- "lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test docs",
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
- "build": "npm run lint:fix && npm run typedef && npm run docs && npm run test"
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: any, handler: any): () => void;
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
@@ -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,qDAFY,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"}
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
  */
@@ -1 +1 @@
1
- {"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACN,IAAI,MAAO,KA6CrB;AAUD;;;;;;;;;;;;;;;GAeG;AACH,qDAJkB,IAAI,iBACH,IAAI,CA0DtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cA+KlB;AAiRD;;;;GAIG;AACH,iCAHW,GAAG,GACD,IAAI,CAuBhB;AAwFD;;;;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;;;;;;;;;uBA9nBwB,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;;2BAEvB,CAAC,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,CAAC,GAAC,GAAG,GAAC,IAAI,gCAAkC,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;qBAG9E,MAAM,KAAG,IAAI;;;;2BAEP,MAAI,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;wBAC1B,MAAI,MAAM,KAAK,MAAM;;;;2BACnB,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;iCACnB,MAAI,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;;;kBAC7B,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"}
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 = (element) => doBindAs(ctx, element ?? ctx.state)
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 = (parent, element) => doBindChildren(ctx, parent, element)
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 = (prop) => doBindAs(ctx, (st) => st[prop])
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 = (attribute) => doBindAttr(ctx.state, attribute ?? ctx.state)
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 = (style) => doBindStyle(ctx.state, style ?? ctx.state)
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 = (element) => doBindSelect(ctx, element ?? ctx.state)
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 = (attribute) => doBindSelectAttr(ctx, attribute ?? ctx.state)
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 = (key) => doSelect(ctx, key)
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 = () => ctx.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 = (update) => ctx.state(Object.assign(ctx.currentValue, update))
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 = (path) => {
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 = (path, value, fillWithObjects = false) => {
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 = (callback) => doSubscribe(ctx, ctx.observers, callback)
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 (ctx, list, listener) {
246
+ function doSubscribe (callback) {
247
+ const ctx = this._ctx
364
248
  const id = ctx.nextId++
365
- list.push({ id, fn: listener })
249
+ ctx.observers.push({ id, fn: callback })
366
250
  return () => {
367
- list.splice(list.findIndex(l => l.id === id), 1)
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
- const doBindSelectAttr = function (ctx, attribute) {
382
- const boundAttr = createBoundAttr(attribute)
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 (state, attribute) {
286
+ function doBindAttr (attribute) {
287
+ attribute = attribute ?? this
399
288
  const boundAttr = createBoundAttr(attribute)
400
- boundAttr.init = (attrName, element) => state.subscribe(() => setAttribute(attrName, attribute(), 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 (state, style) {
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) => state.subscribe(() => { element.style[styleName] = style() })
411
- return boundStyle
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 (ctx, key) {
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 doBindChildren (ctx, parent, element) {
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
- ctx.state.subscribe((_, oldState) => {
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
- ctx.state([ctx.currentValue])
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
- const doBindSelect = (ctx, element) =>
509
- doBind(ctx, element, (elCtx) => subscribeSelect(ctx, updateReplacer(ctx, element, elCtx)))
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
- const doBindAs = (ctx, element) =>
512
- doBind(ctx, element, (elCtx) => ctx.state.subscribe(updateReplacer(ctx, element, elCtx)))
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
- valueState = ctx.currentValue[i] = fnstate(valueState)
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
+ }