@srfnstack/fntags 0.4.0 → 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.0",
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) {
@@ -23,7 +26,6 @@ export function h (tag, ...children) {
23
26
  const nsIndex = hasNs(tag)
24
27
  if (nsIndex > -1) {
25
28
  const { ns, val } = splitNs(tag, nsIndex)
26
- console.log(ns, val)
27
29
  element = document.createElementNS(ns, val)
28
30
  } else {
29
31
  element = document.createElement(tag)
@@ -80,10 +82,12 @@ function hasNs (val) {
80
82
  * You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
81
83
  * not be updated when the state changes because they will not be bound to the cloned element.
82
84
  * All state bindings must be passed in the context to the compiled template to work correctly.
83
- * @param templateFn {function(object): Node}
84
- * @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
+ *
85
89
  */
86
- export const fntemplate = templateFn => {
90
+ export function fntemplate (templateFn) {
87
91
  if (typeof templateFn !== 'function') {
88
92
  throw new Error('You must pass a function to fntemplate. The function must return an html node.')
89
93
  }
@@ -140,17 +144,53 @@ export const fntemplate = templateFn => {
140
144
  }
141
145
  }
142
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
+
143
182
  /**
144
183
  * Create a state object that can be bound to.
145
- * @param initialValue The initial state
146
- * @param mapKey A map function to extract a key from an element in the array. Receives the array value to extract the key from.
147
- * @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.
148
189
  * When getting the state, you get the actual reference to the underlying value.
149
190
  * If you perform modifications to the value, be sure to call the state function with the updated value when you're done
150
191
  * or the changes won't be reflected correctly and binding updates won't be triggered even though the state appears to be correct.
151
- *
152
192
  */
153
- export const fnstate = (initialValue, mapKey) => {
193
+ export function fnstate (initialValue, mapKey) {
154
194
  const ctx = {
155
195
  currentValue: initialValue,
156
196
  observers: [],
@@ -172,6 +212,15 @@ export const fnstate = (initialValue, mapKey) => {
172
212
  }
173
213
  }
174
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
+
175
224
  /**
176
225
  * Bind the values of this state to the given element.
177
226
  * Values are items/elements of an array.
@@ -184,16 +233,7 @@ export const fnstate = (initialValue, mapKey) => {
184
233
  ctx.state.bindChildren = (parent, element, update) => doBindChildren(ctx, parent, element, update)
185
234
 
186
235
  /**
187
- * Bind this state to the given element
188
- *
189
- * @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
190
- * @param [update] If passed this will be executed directly when the state changes with no other intervention
191
- * @returns {(HTMLDivElement|Text)[]|HTMLDivElement|Text}
192
- */
193
- ctx.state.bindAs = (element, update) => doBindAs(ctx, element ?? ctx.state, update)
194
-
195
- /**
196
- * 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.
197
237
  *
198
238
  * Shortcut for `mystate.bindAs((current)=> current[prop])`
199
239
  *
@@ -610,8 +650,10 @@ const evaluateElement = (element, value) => {
610
650
 
611
651
  /**
612
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
613
655
  */
614
- export const renderNode = (node) => {
656
+ export function renderNode (node) {
615
657
  if (node && node.isTemplatePlaceholder) {
616
658
  const element = h('div')
617
659
  node(element, 'node')
@@ -720,22 +762,34 @@ const setStyle = (style, styleValue, element) => {
720
762
  element.style[style] = styleValue && styleValue.toString()
721
763
  }
722
764
 
723
- 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
+
724
774
  /**
725
775
  * helper to get the attr object
776
+ * @param {any} children
777
+ * @return {object} the attr object or an empty object
726
778
  */
727
- 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
+ }
728
782
 
729
783
  /**
730
784
  * A function to create an element with a pre-defined style.
731
785
  * For example, the flex* elements in fnelements.
732
786
  *
733
- * @param style
734
- * @param tag
735
- * @param children
736
- * @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
737
791
  */
738
- export const styled = (style, tag, children) => {
792
+ export function styled (style, tag, children) {
739
793
  const firstChild = children[0]
740
794
  if (isAttrs(firstChild)) {
741
795
  if (typeof firstChild.style === 'string') {