@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 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.5.3",
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/** --headless -b chrome",
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
- "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
@@ -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: any, handler: any): () => void;
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("./fntags.mjs").FnState<PathParameters>}
69
+ * @type {import('./fntags.mjs').FnState<PathParameters>}
89
70
  */
90
- export const pathParameters: import("./fntags.mjs").FnState<PathParameters>;
71
+ export const pathParameters: import('./fntags.mjs').FnState<PathParameters>;
91
72
  /**
92
73
  * The path information for a route
93
- * @typedef {{currentRoute: string, rootPath: string, context: any}} PathState
74
+ * @typedef {{currentPath: string, rootPath: string, context: any}} PathState
94
75
  */
95
76
  /**
96
77
  * The current path state
97
- * @type {import("./fntags.mjs").FnState<PathState>}
78
+ * @type {import('./fntags.mjs').FnState<PathState>}
98
79
  */
99
- export const pathState: import("./fntags.mjs").FnState<PathState>;
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
- currentRoute: string;
107
+ currentPath: string;
127
108
  rootPath: string;
128
109
  context: any;
129
110
  };
@@ -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,CAwB3B;AAeD;;;;;;;;;;GAUG;AACH;eARW,MAAM;WACN,MAAM;qBACE,KAAK,gBAAgB,MAAM,KAAG,IAAI,GAAC,IAAI;kBACxC,IAAI,UAAU,MAAM,KAAG,IAAI;iBAClC,OAAO;uBACA,MAAM,KAAG,MAAM;IACrB,WAAW,CA0DtB;AAwBD;;;;GAIG;AACH,oCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,iBAAiB,CAuB7B;AAED;;;;;;GAMG;AACH,4BALW,MAAM,YACN,GAAG,YACH,OAAO,WACP,OAAO,QA6CjB;AA6DD;;;;;;GAMG;AACH,qDAFY,MAAI,IAAI,CAanB;AAED;;;GAGG;AACH,sCAFW,MAAM,QAOhB;AApFD;;;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;AAEJ;;GAEG;AACH;;;GAGG;AACH,gCAFU,UAAU,CAEgC;AACpD;;;GAGG;AACH,+BAFU,UAAU,CAE8B;AAClD;;;GAGG;AACH,kCAFU,UAAU,CAEoC;;;;;;;;wBA/B3C;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAC;yBAetD,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
@@ -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 = pathState().currentRoute
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
- const paramStart = part.indexOf(':')
176
- if (paramStart > -1) {
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
- currentRoute: route.split(/[#?]/)[0],
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
- currentRoute: route.split(/[#?]/)[0],
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("./fntags.mjs").FnState<PathParameters>}
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 {{currentRoute: string, rootPath: string, context: any}} PathState
199
+ * @typedef {{currentPath: string, rootPath: string, context: any}} PathState
286
200
  */
287
201
 
288
202
  /**
289
203
  * The current path state
290
- * @type {import("./fntags.mjs").FnState<PathState>}
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
- currentRoute: ensureOnlyLeadingSlash(window.location.pathname),
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
- currentRoute: ensureOnlyLeadingSlash(window.location.pathname.replace(new RegExp('^' + rootPath), '')) || '/'
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
- currentRoute: ensureOnlyLeadingSlash(window.location.pathname.replace(new RegExp('^' + pathState().rootPath), '')) || '/'
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.currentRoute, oldPathState.context, true, true)
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((path).replace(/\/\$[^/]+(\/?)/g, '/[^/]+$1') + '$')
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.
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;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 boundAttr = createBoundAttr(attribute)
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) => state.subscribe(() => setAttribute(attrName, attribute(), 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) => state.subscribe(() => { element.style[styleName] = style() })
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
- valueState = ctx.currentValue[i] = fnstate(valueState)
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