@srfnstack/fntags 0.4.1 → 0.4.2

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
@@ -38,52 +38,45 @@ Check out the [documentation](https://srfnstack.github.io/fntags) to learn more!
38
38
  ### f'n examples
39
39
  <hr>
40
40
 
41
- Components are plain functions
42
- ```javascript
43
- import { div } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fnelements.min.mjs'
41
+ Start a new app with one file
42
+ ```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>
50
+ ```
44
51
 
52
+ Make re-usable, customizable components using plain js functions
53
+ ```javascript
45
54
  const hello = name => div('Hello ', name)
46
55
 
47
56
  document.body.append( hello('world!') )
48
57
  ```
49
58
 
50
- Two-way binding is a breeze with the bind functions provided by fnstate objects.
59
+ Explicit two-way state binding
51
60
  ```javascript
52
- import { fnstate } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fntags.min.mjs'
53
- import { div, input, br } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fnelements.min.mjs'
54
-
55
- export const userName = fnstate('world')
56
- export const appColor = fnstate('MediumTurquoise')
57
-
58
- document.body.append(
59
- div( { style: { color: appColor.bindStyle() } },
60
- 'Hello ', userName.bindAs(), '!'
61
- ),
62
- br(),
63
- input({
64
- value: userName.bindAttr(),
65
- oninput: e => userName(e.target.value)
66
- }),
67
- br(),
68
- input({
69
- value: appColor.bindAttr(),
70
- oninput: e => appColor(e.target.value)
71
- }),
72
- )
73
- ```
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'
74
63
 
75
- ### Required HTML
76
- Unfortunately browsers lack the ability to render a js file directly, and thus you still need a tiny bit of html to bootstrap your app.
64
+ const helloInput = () => {
65
+ const name = fnstate('World')
77
66
 
78
- Here's an example of the only html you need to write for your entire application.
67
+ const nameInput = input({
68
+ value: name.bindAttr(),
69
+ oninput (){ name(nameInput.value) }
70
+ })
79
71
 
80
- Now that these two lines are there you're set to write sweet sweet es6+ and no more html.
72
+ return div(
73
+ div('Hello ', name.bindAs(), '!'),
74
+ br(),
75
+ nameInput
76
+ )
77
+ }
81
78
 
82
- ```html
83
- <html><body><script type="module">
84
- import { div } from 'https://cdn.jsdelivr.net/npm/@srfnstack/fntags@0.3.3/src/fnelements.min.mjs'
85
- document.body.append(div('hello world!'))
86
- </script></body></html>
79
+ document.body.append(helloInput())
87
80
  ```
88
81
 
89
82
  ### Benchmark
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srfnstack/fntags",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "author": "Robert Kempton <r@snow87.com>",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/srfnstack/fntags",
@@ -30,15 +30,21 @@
30
30
  "state-management"
31
31
  ],
32
32
  "devDependencies": {
33
- "cypress": "10.3.0",
33
+ "cypress": "13.6.1",
34
34
  "pre-commit": "^1.2.2",
35
- "standard": "^16.0.4"
35
+ "standard": "^17.1.0",
36
+ "typedoc": "^0.25.4",
37
+ "typedoc-plugin-markdown": "^3.17.1",
38
+ "typescript": "^5.3.3"
36
39
  },
37
40
  "scripts": {
38
41
  "test": "cp src/* docs/lib/ && npm run lint && cypress run --spec test/** --headless -b chrome",
39
42
  "cypress": "cypress run --spec test/** -b chrome",
40
43
  "lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test docs",
41
- "lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test docs"
44
+ "lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test docs",
45
+ "typedef": "tsc",
46
+ "docs": "typedoc --plugin typedoc-plugin-markdown --out docs/types ./src/*.mjs",
47
+ "build": "npm run lint:fix && npm run typedef && npm run docs && npm run test"
42
48
  },
43
49
  "pre-commit": [
44
50
  "lint",
package/src/fnroute.mjs CHANGED
@@ -1,7 +1,11 @@
1
+ /// <reference path="fntags.mjs" name="fntags"/>
2
+ /**
3
+ * @module fnroute
4
+ */
1
5
  import { fnstate, getAttrs, h, isAttrs, renderNode } from './fntags.mjs'
2
6
 
3
7
  /**
4
- * An element that is displayed only if the the current route starts with elements path attribute.
8
+ * An element that is displayed only if the current route starts with elements path attribute.
5
9
  *
6
10
  * For example,
7
11
  * route({path: "/proc"},
@@ -24,10 +28,10 @@ import { fnstate, getAttrs, h, isAttrs, renderNode } from './fntags.mjs'
24
28
  * )
25
29
  * )
26
30
  *
27
- * @param children The attributes and children of this element.
28
- * @returns HTMLDivElement
31
+ * @param {any} children The attributes and children of this element.
32
+ * @returns {HTMLDivElement} A div element that will only be displayed if the current route starts with the path attribute.
29
33
  */
30
- export const route = (...children) => {
34
+ export function route (...children) {
31
35
  const attrs = getAttrs(children)
32
36
  children = children.filter(c => !isAttrs(c))
33
37
  const routeEl = h('div', attrs)
@@ -50,9 +54,10 @@ export const route = (...children) => {
50
54
  /**
51
55
  * An element that only renders the first route that matches and updates when the route is changed
52
56
  * The primary purpose of this element is to provide catchall routes for not found pages and path variables
53
- * @param children
57
+ * @param {any} children
58
+ * @returns {HTMLDivElement}
54
59
  */
55
- export const routeSwitch = (...children) => {
60
+ export function routeSwitch (...children) {
56
61
  const sw = h('div', getAttrs(children))
57
62
 
58
63
  return pathState.bindAs(
@@ -89,7 +94,18 @@ function stripParameterValues (currentRoute) {
89
94
 
90
95
  const moduleCache = {}
91
96
 
92
- export const modRouter = ({ routePath, attrs, onerror, frame, sendRawPath, formatPath }) => {
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} options.onerror A function that will be called if the route fails to load. The function receives the error and the current pathState object.
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 }) {
93
109
  const container = h('div', attrs || {})
94
110
  if (!routePath) {
95
111
  throw new Error('You must provide a root url for modRouter. Routes in the ui will be looked up relative to this url.')
@@ -104,8 +120,8 @@ export const modRouter = ({ routePath, attrs, onerror, frame, sendRawPath, forma
104
120
  }
105
121
  const filePath = path ? routePath + ensureOnlyLeadingSlash(path) : routePath
106
122
 
107
- const p = moduleCache[filePath]
108
- ? Promise.resolve(moduleCache[filePath])
123
+ const p = moduleCache[filePath]
124
+ ? Promise.resolve(moduleCache[filePath])
109
125
  : import(filePath).then(m => {
110
126
  moduleCache[filePath] = m
111
127
  return m
@@ -171,9 +187,10 @@ function updatePathParameters () {
171
187
 
172
188
  /**
173
189
  * A link element that is a link to another route in this single page app
174
- * @param children The attributes of the anchor element and any children
190
+ * @param {any} children The attributes of the anchor element and any children
191
+ * @returns {HTMLAnchorElement} An anchor element that will navigate to the specified route when clicked
175
192
  */
176
- export const fnlink = (...children) => {
193
+ export function fnlink (...children) {
177
194
  let context = null
178
195
  if (children[0] && children[0].context) {
179
196
  context = children[0].context
@@ -198,12 +215,12 @@ export const fnlink = (...children) => {
198
215
 
199
216
  /**
200
217
  * A function to navigate to the specified route
201
- * @param route The route to navigate to
202
- * @param context Data related to the route change
203
- * @param replace Whether to replace the state or push it. pushState is used by default.
204
- * @param silent Prevent route change events from being emitted for this route change
218
+ * @param {string} route The route to navigate to
219
+ * @param {any} context Data related to the route change
220
+ * @param {boolean} replace Whether to replace the state or push it. pushState is used by default.
221
+ * @param {boolean} silent Prevent route change events from being emitted for this route change
205
222
  */
206
- export const goTo = (route, context, replace = false, silent = false) => {
223
+ export function goTo (route, context, replace = false, silent = false) {
207
224
  const newPath = window.location.origin + makePath(route)
208
225
 
209
226
  const patch = {
@@ -252,8 +269,26 @@ const ensureOnlyLeadingSlash = (part) => removeTrailingSlash(part.startsWith('/'
252
269
 
253
270
  const removeTrailingSlash = part => part.endsWith('/') && part.length > 1 ? part.slice(0, -1) : part
254
271
 
272
+ /**
273
+ * Key value pairs of path parameter names to their values
274
+ * @typedef {Object} PathParameters
275
+ */
276
+
277
+ /**
278
+ * The path parameters of the current route
279
+ * @type {import("./fntags.mjs").FnState<PathParameters>}
280
+ */
255
281
  export const pathParameters = fnstate({})
256
282
 
283
+ /**
284
+ * The path information for a route
285
+ * @typedef {{currentRoute: string, rootPath: string, context: any}} PathState
286
+ */
287
+
288
+ /**
289
+ * The current path state
290
+ * @type {import("./fntags.mjs").FnState<PathState>}
291
+ */
257
292
  export const pathState = fnstate(
258
293
  {
259
294
  rootPath: ensureOnlyLeadingSlash(window.location.pathname),
@@ -261,8 +296,23 @@ export const pathState = fnstate(
261
296
  context: null
262
297
  })
263
298
 
299
+ /**
300
+ * @typedef {string} RouteEvent
301
+ */
302
+ /**
303
+ * Before the route is changed
304
+ * @type {RouteEvent}
305
+ */
264
306
  export const beforeRouteChange = 'beforeRouteChange'
307
+ /**
308
+ * After the route is changed
309
+ * @type {RouteEvent}
310
+ */
265
311
  export const afterRouteChange = 'afterRouteChange'
312
+ /**
313
+ * After the route is changed and the route element is rendered
314
+ * @type {RouteEvent}
315
+ */
266
316
  export const routeChangeComplete = 'routeChangeComplete'
267
317
  const eventListeners = {
268
318
  [beforeRouteChange]: [],
@@ -279,9 +329,9 @@ const emit = (event, newPathState, oldPathState) => {
279
329
  * @param event a string event to listen for
280
330
  * @param handler A function that will be called when the event occurs.
281
331
  * The function receives the new and old pathState objects, in that order.
282
- * @return {function()} a function to stop listening with the passed handler.
332
+ * @return {()=>void} a function to stop listening with the passed handler.
283
333
  */
284
- export const listenFor = (event, handler) => {
334
+ export function listenFor (event, handler) {
285
335
  if (!eventListeners[event]) {
286
336
  throw new Error(`Invalid event. Must be one of ${Object.keys(eventListeners)}`)
287
337
  }
@@ -296,12 +346,14 @@ export const listenFor = (event, handler) => {
296
346
 
297
347
  /**
298
348
  * 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.
349
+ * @param {string} rootPath The root path of the app
299
350
  */
300
- export const setRootPath = (rootPath) =>
301
- pathState.assign({
351
+ export function setRootPath (rootPath) {
352
+ return pathState.assign({
302
353
  rootPath: ensureOnlyLeadingSlash(rootPath),
303
354
  currentRoute: ensureOnlyLeadingSlash(window.location.pathname.replace(new RegExp('^' + rootPath), '')) || '/'
304
355
  })
356
+ }
305
357
 
306
358
  window.addEventListener(
307
359
  'popstate',
package/src/fntags.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @module fntags
3
+ */
1
4
  /**
2
5
  * A function to create dom elements with the given attributes and children.
3
6
  *
@@ -12,9 +15,9 @@
12
15
  *
13
16
  * The rest of the arguments will be considered children of this element and appended to it in the same order as passed.
14
17
  *
15
- * @param tag html tag to use when created the element
16
- * @param children optional attributes object and children for the element
17
- * @returns HTMLElement an html element
18
+ * @param {string} tag html tag to use when created the element
19
+ * @param {object[]?|Node[]?} children optional attributes object and children for the element
20
+ * @return {HTMLElement} an html element
18
21
  *
19
22
  */
20
23
  export function h (tag, ...children) {
@@ -79,10 +82,12 @@ function hasNs (val) {
79
82
  * You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
80
83
  * not be updated when the state changes because they will not be bound to the cloned element.
81
84
  * All state bindings must be passed in the context to the compiled template to work correctly.
82
- * @param templateFn {function(object): Node}
83
- * @return {function(*): Node}
85
+ *
86
+ * @param {(any)=>Node} templateFn A function that returns an html node.
87
+ * @return {(any)=>Node} A function that takes a context object and returns a rendered node.
88
+ *
84
89
  */
85
- export const fntemplate = templateFn => {
90
+ export function fntemplate (templateFn) {
86
91
  if (typeof templateFn !== 'function') {
87
92
  throw new Error('You must pass a function to fntemplate. The function must return an html node.')
88
93
  }
@@ -139,17 +144,53 @@ export const fntemplate = templateFn => {
139
144
  }
140
145
  }
141
146
 
147
+ /**
148
+ * @template T The type of data stored in the state container
149
+ * @typedef FnStateObj A container for a state value that can be bound to.
150
+ * @property {(element: (T)=>void|Node|any?, update: (Node)=>void?) => Node|() => Node} bindAs Bind this state to the given element. This causes the element to update when state changes.
151
+ * If called with no parameters, the state's value will be rendered as an element. If the first parameters is not a function,
152
+ * the second parameter (the update function) must be provided and must be a function. This function receives the node the state is bound to.
153
+ * @property {(parent: Node,element: Node|any, update: (Node)=>void?)=> Node|()=> Node} bindChildren Bind the values of this state to the given element.
154
+ * Values are items/elements of an array.
155
+ * If the current value is not an array, this will behave the same as bindAs.
156
+ * @property {(prop: string)=>Node|()=>Node} bindProp Bind to a property of an object stored in this state instead of the state itself.
157
+ * Shortcut for `mystate.bindAs((current)=> current[prop])`
158
+ * @property {(attribute: string)=>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, update: (Node)=>void?)=>Node|()=>Node} bindSelect Bind selected state to an element
161
+ * @property {(attribute: string)=>any} bindSelectAttr Bind selected state to an attribute
162
+ * @property {(key: any)=>void} select Mark the element with the given key as selected
163
+ * where the key is identified using the mapKey function passed on creation of the fnstate.
164
+ * This causes the bound select functions to be executed.
165
+ * @property {()=> any} selected Get the currently selected key
166
+ * @property {(update: T)=>void} assign Perform an Object.assign() on the current state using the provided update, triggers
167
+ * a state change and is a shortcut for `mystate(Object.assign(mystate(), update))`
168
+ * @property {(path: string)=>any} getPath Get a value at the given property path, an error is thrown if the value is not an object
169
+ * This returns a reference to the real current value. If you perform any modifications to the object, be sure to call setPath after you're done or the changes
170
+ * will not be reflected correctly.
171
+ * @property {(path: string, value: any, fillWithObjects: boolean)=>void} setPath Set a value at the given property path
172
+ * @property {((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
+ * @property {} isFnState A flag to indicate that this is an fnstate object
175
+ */
176
+
177
+ /**
178
+ * @template T The type of data stored in the state container
179
+ * @typedef {FnStateObj<T> & (newState: T?)=>T} FnState A container for a state value that can be bound to.
180
+ */
181
+
142
182
  /**
143
183
  * Create a state object that can be bound to.
144
- * @param initialValue The initial state
145
- * @param mapKey A map function to extract a key from an element in the array. Receives the array value to extract the key from.
146
- * @returns function A function that can be used to get and set the state.
184
+ * @template T
185
+ * @param {T|any} initialValue The initial state
186
+ * @param {function(T): any?} mapKey A map function to extract a key from an element in the array. Receives the array value to extract the key from.
187
+ * A key can be any unique value.
188
+ * @return {FnState<T>} A function that can be used to get and set the state.
147
189
  * When getting the state, you get the actual reference to the underlying value.
148
190
  * If you perform modifications to the value, be sure to call the state function with the updated value when you're done
149
191
  * or the changes won't be reflected correctly and binding updates won't be triggered even though the state appears to be correct.
150
- *
151
192
  */
152
- export const fnstate = (initialValue, mapKey) => {
193
+ export function fnstate (initialValue, mapKey) {
153
194
  const ctx = {
154
195
  currentValue: initialValue,
155
196
  observers: [],
@@ -171,6 +212,15 @@ export const fnstate = (initialValue, mapKey) => {
171
212
  }
172
213
  }
173
214
 
215
+ /**
216
+ * Bind this state to the given element
217
+ *
218
+ * @param [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
+ * @param [update] If passed this will be executed directly when the state changes with no other intervention
220
+ * @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
221
+ */
222
+ ctx.state.bindAs = (element, update) => doBindAs(ctx, element ?? ctx.state, update)
223
+
174
224
  /**
175
225
  * Bind the values of this state to the given element.
176
226
  * Values are items/elements of an array.
@@ -183,16 +233,7 @@ export const fnstate = (initialValue, mapKey) => {
183
233
  ctx.state.bindChildren = (parent, element, update) => doBindChildren(ctx, parent, element, update)
184
234
 
185
235
  /**
186
- * Bind this state to the given element
187
- *
188
- * @param [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
189
- * @param [update] If passed this will be executed directly when the state changes with no other intervention
190
- * @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
191
- */
192
- ctx.state.bindAs = (element, update) => doBindAs(ctx, element ?? ctx.state, update)
193
-
194
- /**
195
- * Bind a property of an object stored in this state as a simple value.
236
+ * Bind to a property of an object stored in this state instead of the state itself.
196
237
  *
197
238
  * Shortcut for `mystate.bindAs((current)=> current[prop])`
198
239
  *
@@ -609,8 +650,10 @@ const evaluateElement = (element, value) => {
609
650
 
610
651
  /**
611
652
  * Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes
653
+ * @param {any} node The node to render
654
+ * @returns {Node} The rendered node
612
655
  */
613
- export const renderNode = (node) => {
656
+ export function renderNode (node) {
614
657
  if (node && node.isTemplatePlaceholder) {
615
658
  const element = h('div')
616
659
  node(element, 'node')
@@ -719,22 +762,34 @@ const setStyle = (style, styleValue, element) => {
719
762
  element.style[style] = styleValue && styleValue.toString()
720
763
  }
721
764
 
722
- export const isAttrs = (val) => val && typeof val === 'object' && val.nodeType === undefined && !Array.isArray(val) && typeof val.then !== 'function'
765
+ /**
766
+ * Check if the given value is an object that can be used as attributes
767
+ * @param {any} val The value to check
768
+ * @returns {boolean} true if the value is an object that can be used as attributes
769
+ */
770
+ export function isAttrs (val) {
771
+ return val && typeof val === 'object' && val.nodeType === undefined && !Array.isArray(val) && typeof val.then !== 'function'
772
+ }
773
+
723
774
  /**
724
775
  * helper to get the attr object
776
+ * @param {any} children
777
+ * @return {object} the attr object or an empty object
725
778
  */
726
- export const getAttrs = (children) => Array.isArray(children) && isAttrs(children[0]) ? children[0] : {}
779
+ export function getAttrs (children) {
780
+ return Array.isArray(children) && isAttrs(children[0]) ? children[0] : {}
781
+ }
727
782
 
728
783
  /**
729
784
  * A function to create an element with a pre-defined style.
730
785
  * For example, the flex* elements in fnelements.
731
786
  *
732
- * @param style
733
- * @param tag
734
- * @param children
735
- * @return {*}
787
+ * @param {object|string} style The style to apply to the element
788
+ * @param {string} tag The tag to use when creating the element
789
+ * @param {object|object[]?} children The children to append to the element
790
+ * @return {*} The styled element
736
791
  */
737
- export const styled = (style, tag, children) => {
792
+ export function styled (style, tag, children) {
738
793
  const firstChild = children[0]
739
794
  if (isAttrs(firstChild)) {
740
795
  if (typeof firstChild.style === 'string') {