@pfern/elements 0.1.1 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/elements.js +215 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -221,7 +221,7 @@ Each tag function (e.g. `div`, `button`, `svg`) includes a `@typedef` and MDN-so
221
221
  ## Installation
222
222
 
223
223
  ```bash
224
- npm install elements-js
224
+ npm install @pfern/elements
225
225
  ```
226
226
 
227
227
  Or clone the repo and use as an ES module:
package/elements.js CHANGED
@@ -61,14 +61,14 @@ const diffTree = (a, b) => {
61
61
  }
62
62
  }
63
63
 
64
-
65
64
  /**
66
65
  * Compares the children of two vnodes and returns patch list.
67
66
  *
68
67
  * @param {Array} aChildren - Previous vnode children
69
68
  * @param {Array} bChildren - New vnode children
70
69
  * @returns {Array} patches - One per child node
71
- */const diffChildren = (aChildren, bChildren) => {
70
+ */
71
+ const diffChildren = (aChildren, bChildren) => {
72
72
  const patches = []
73
73
  const len = Math.max(aChildren.length, bChildren.length)
74
74
  for (let i = 0; i < len; i++) {
@@ -85,6 +85,8 @@ const diffTree = (a, b) => {
85
85
  * *if* the listener returns a vnode (to support declarative form updates).
86
86
  * - Handlers for these event types receive `(elements, event)` as arguments,
87
87
  * where `elements` is `event.target.elements` if available.
88
+ * - Async handlers are supported: if the listener returns a Promise,
89
+ * it will be awaited and the resulting vnode (if any) will be rendered.
88
90
  *
89
91
  * @param {HTMLElement} el - The DOM element to receive props
90
92
  * @param {Object} props - Attributes and event listeners to assign
@@ -92,7 +94,7 @@ const diffTree = (a, b) => {
92
94
  const assignProperties = (el, props) =>
93
95
  Object.entries(props).forEach(([key, value]) => {
94
96
  if (key.startsWith('on') && typeof value === 'function') {
95
- el[key] = (...args) => {
97
+ el[key] = async (...args) => {
96
98
  let target = el
97
99
  while (target && !target.__root) target = target.parentNode
98
100
  if (!target) return
@@ -102,9 +104,9 @@ const assignProperties = (el, props) =>
102
104
  const isFormEvent = /^(oninput|onsubmit|onchange)$/.test(key)
103
105
  const elements = isFormEvent && event?.target?.elements || null
104
106
 
105
- const result = isFormEvent
107
+ const result = await (isFormEvent
106
108
  ? value.call(el, elements, event)
107
- : value.call(el, event)
109
+ : value.call(el, event))
108
110
 
109
111
  if (isFormEvent && result !== undefined) {
110
112
  event.preventDefault()
@@ -119,11 +121,12 @@ const assignProperties = (el, props) =>
119
121
 
120
122
  if (DEBUG && result !== undefined && !Array.isArray(result)) {
121
123
  isFormEvent && event.preventDefault()
122
- DEBUG && console.warn(
123
- `Listener '${key}' on <${el.tagName.toLowerCase()}> returned "${result}".\n`
124
- + 'If you intended a UI update, return a vnode array like: div({}, ...).\n'
125
- + 'Otherwise, return undefined (or nothing) for native event listener behavior.'
126
- )
124
+ DEBUG
125
+ && console.warn(
126
+ `Listener '${key}' on <${el.tagName.toLowerCase()}> returned "${result}".\n`
127
+ + 'If you intended a UI update, return a vnode array like: div({}, ...).\n'
128
+ + 'Otherwise, return undefined (or nothing) for native event listener behavior.'
129
+ )
127
130
  }
128
131
 
129
132
  if (Array.isArray(result)) {
@@ -153,8 +156,10 @@ const assignProperties = (el, props) =>
153
156
  el.setAttribute(key, value)
154
157
  }
155
158
  } catch {
156
- DEBUG && console.warn(
157
- `Illegal DOM property assignment for ${el.tagName}: ${key}: ${value}`)
159
+ DEBUG
160
+ && console.warn(
161
+ `Illegal DOM property assignment for ${el.tagName}: ${key}: ${value}`
162
+ )
158
163
  }
159
164
  }
160
165
  })
@@ -194,9 +199,12 @@ const renderTree = (node, isRoot = true) => {
194
199
  }
195
200
 
196
201
  let el =
197
- tag === 'html' ? document.documentElement
198
- : tag === 'head' ? document.head
199
- : tag === 'body' ? document.body
202
+ tag === 'html'
203
+ ? document.documentElement
204
+ : tag === 'head'
205
+ ? document.head
206
+ : tag === 'body'
207
+ ? document.body
200
208
  : svgTagNames.includes(tag)
201
209
  ? document.createElementNS(svgNS, tag)
202
210
  : document.createElement(tag)
@@ -311,88 +319,232 @@ export const component = fn => {
311
319
 
312
320
  const htmlTagNames = [
313
321
  // Document metadata
314
- 'html', 'head', 'base', 'link', 'meta', 'title',
322
+ 'html',
323
+ 'head',
324
+ 'base',
325
+ 'link',
326
+ 'meta',
327
+ 'title',
315
328
 
316
329
  // Sections
317
- 'body', 'header', 'hgroup', 'nav', 'main', 'section', 'article',
318
- 'aside', 'footer', 'address',
330
+ 'body',
331
+ 'header',
332
+ 'hgroup',
333
+ 'nav',
334
+ 'main',
335
+ 'section',
336
+ 'article',
337
+ 'aside',
338
+ 'footer',
339
+ 'address',
319
340
 
320
341
  // Text content
321
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'hr', 'menu', 'pre', 'blockquote',
322
- 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'figure', 'figcaption',
342
+ 'h1',
343
+ 'h2',
344
+ 'h3',
345
+ 'h4',
346
+ 'h5',
347
+ 'h6',
348
+ 'p',
349
+ 'hr',
350
+ 'menu',
351
+ 'pre',
352
+ 'blockquote',
353
+ 'ol',
354
+ 'ul',
355
+ 'li',
356
+ 'dl',
357
+ 'dt',
358
+ 'dd',
359
+ 'figure',
360
+ 'figcaption',
323
361
  'div',
324
362
 
325
363
  // Inline text semantics
326
- 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code',
327
- 'data', 'dfn', 'em', 'i', 'kbd', 'mark', 'q', 'rb', 'rp',
328
- 'rt', 'rtc', 'ruby', 's', 'samp', 'small', 'span', 'strong',
329
- 'sub', 'sup', 'time', 'u', 'var', 'wbr',
364
+ 'a',
365
+ 'abbr',
366
+ 'b',
367
+ 'bdi',
368
+ 'bdo',
369
+ 'br',
370
+ 'cite',
371
+ 'code',
372
+ 'data',
373
+ 'dfn',
374
+ 'em',
375
+ 'i',
376
+ 'kbd',
377
+ 'mark',
378
+ 'q',
379
+ 'rb',
380
+ 'rp',
381
+ 'rt',
382
+ 'rtc',
383
+ 'ruby',
384
+ 's',
385
+ 'samp',
386
+ 'small',
387
+ 'span',
388
+ 'strong',
389
+ 'sub',
390
+ 'sup',
391
+ 'time',
392
+ 'u',
393
+ 'var',
394
+ 'wbr',
330
395
 
331
396
  // Edits
332
- 'ins', 'del',
397
+ 'ins',
398
+ 'del',
333
399
 
334
400
  // Embedded content
335
- 'img', 'iframe', 'embed', 'object', 'param', 'video', 'audio',
336
- 'source', 'track', 'picture',
401
+ 'img',
402
+ 'iframe',
403
+ 'embed',
404
+ 'object',
405
+ 'param',
406
+ 'video',
407
+ 'audio',
408
+ 'source',
409
+ 'track',
410
+ 'picture',
337
411
 
338
412
  // Table content
339
- 'table', 'caption', 'thead', 'tbody', 'tfoot', 'tr',
340
- 'th', 'td', 'colgroup', 'col',
413
+ 'table',
414
+ 'caption',
415
+ 'thead',
416
+ 'tbody',
417
+ 'tfoot',
418
+ 'tr',
419
+ 'th',
420
+ 'td',
421
+ 'colgroup',
422
+ 'col',
341
423
 
342
424
  // Forms
343
- 'form', 'fieldset', 'legend', 'label', 'input', 'button',
344
- 'select', 'datalist', 'optgroup', 'option', 'textarea',
345
- 'output', 'progress', 'meter',
425
+ 'form',
426
+ 'fieldset',
427
+ 'legend',
428
+ 'label',
429
+ 'input',
430
+ 'button',
431
+ 'select',
432
+ 'datalist',
433
+ 'optgroup',
434
+ 'option',
435
+ 'textarea',
436
+ 'output',
437
+ 'progress',
438
+ 'meter',
346
439
 
347
440
  // Interactive elements
348
- 'details', 'search', 'summary', 'dialog', 'slot', 'template',
441
+ 'details',
442
+ 'search',
443
+ 'summary',
444
+ 'dialog',
445
+ 'slot',
446
+ 'template',
349
447
 
350
448
  // Scripting and style
351
- 'script', 'noscript', 'style',
449
+ 'script',
450
+ 'noscript',
451
+ 'style',
352
452
 
353
453
  // Web components and others
354
- 'canvas', 'picture', 'map', 'area', 'slot'
454
+ 'canvas',
455
+ 'picture',
456
+ 'map',
457
+ 'area',
458
+ 'slot'
355
459
  ]
356
460
 
357
461
  const svgTagNames = [
358
462
  // Animation elements
359
- 'a', 'animate', 'animateMotion', 'animateTransform', 'mpath', 'set',
463
+ 'a',
464
+ 'animate',
465
+ 'animateMotion',
466
+ 'animateTransform',
467
+ 'mpath',
468
+ 'set',
360
469
 
361
470
  // Basic shapes
362
- 'circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect',
471
+ 'circle',
472
+ 'ellipse',
473
+ 'line',
474
+ 'path',
475
+ 'polygon',
476
+ 'polyline',
477
+ 'rect',
363
478
 
364
479
  // Container / structural
365
- 'defs', 'g', 'marker', 'mask', 'pattern', 'svg', 'switch', 'symbol', 'use',
480
+ 'defs',
481
+ 'g',
482
+ 'marker',
483
+ 'mask',
484
+ 'pattern',
485
+ 'svg',
486
+ 'switch',
487
+ 'symbol',
488
+ 'use',
366
489
 
367
490
  // Descriptive
368
- 'desc', 'metadata', 'title',
491
+ 'desc',
492
+ 'metadata',
493
+ 'title',
369
494
 
370
495
  // Filter primitives
371
- 'filter', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite',
372
- 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap',
373
- 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB',
374
- 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge',
375
- 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight',
376
- 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence',
496
+ 'filter',
497
+ 'feBlend',
498
+ 'feColorMatrix',
499
+ 'feComponentTransfer',
500
+ 'feComposite',
501
+ 'feConvolveMatrix',
502
+ 'feDiffuseLighting',
503
+ 'feDisplacementMap',
504
+ 'feDistantLight',
505
+ 'feDropShadow',
506
+ 'feFlood',
507
+ 'feFuncA',
508
+ 'feFuncB',
509
+ 'feFuncG',
510
+ 'feFuncR',
511
+ 'feGaussianBlur',
512
+ 'feImage',
513
+ 'feMerge',
514
+ 'feMergeNode',
515
+ 'feMorphology',
516
+ 'feOffset',
517
+ 'fePointLight',
518
+ 'feSpecularLighting',
519
+ 'feSpotLight',
520
+ 'feTile',
521
+ 'feTurbulence',
377
522
 
378
523
  // Gradient / paint servers
379
- 'linearGradient', 'radialGradient', 'stop',
524
+ 'linearGradient',
525
+ 'radialGradient',
526
+ 'stop',
380
527
 
381
528
  // Graphics elements
382
- 'image', 'foreignObject', // included in graphics section as non‑standard children
529
+ 'image',
530
+ 'foreignObject', // included in graphics section as non‑standard children
383
531
 
384
532
  // Text and text-path
385
- 'text', 'textPath', 'tspan',
533
+ 'text',
534
+ 'textPath',
535
+ 'tspan',
386
536
 
387
537
  // Scripting/style
388
- 'script', 'style',
538
+ 'script',
539
+ 'style',
389
540
 
390
541
  // View
391
542
  'view'
392
543
  ]
393
544
 
394
545
  const tagNames = [...htmlTagNames, ...svgTagNames]
395
- const isPropsObject = x => typeof x === 'object'
546
+ const isPropsObject = x =>
547
+ typeof x === 'object'
396
548
  && x !== null
397
549
  && !Array.isArray(x)
398
550
  && !(typeof Node !== 'undefined' && x instanceof Node)
@@ -422,18 +574,20 @@ const isPropsObject = x => typeof x === 'object'
422
574
  *
423
575
  * @type {Record<string, ElementHelper>}
424
576
  */
425
- export const elements = tagNames.reduce((acc, tag) => ({
426
- ...acc,
427
- [tag]: (propsOrChild, ...children) => {
428
- const props = isPropsObject(propsOrChild) ? propsOrChild : {}
429
- const actualChildren = props === propsOrChild
430
- ? children
431
- : [propsOrChild, ...children]
432
- return [tag, props, ...actualChildren]
577
+ export const elements = tagNames.reduce(
578
+ (acc, tag) => ({
579
+ ...acc,
580
+ [tag]: (propsOrChild, ...children) => {
581
+ const props = isPropsObject(propsOrChild) ? propsOrChild : {}
582
+ const actualChildren =
583
+ props === propsOrChild ? children : [propsOrChild, ...children]
584
+ return [tag, props, ...actualChildren]
585
+ }
586
+ }),
587
+ {
588
+ fragment: (...children) => ['fragment', {}, ...children]
433
589
  }
434
- }), {
435
- fragment: (...children) => ['fragment', {}, ...children]
436
- })
590
+ )
437
591
 
438
592
  /**
439
593
  * <html>
@@ -1928,4 +2082,3 @@ export const view = elements.view
1928
2082
 
1929
2083
  // TODO: MathML
1930
2084
  // https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element
1931
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pfern/elements",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A minimalist, pure functional declarative UI toolkit.",
5
5
  "main": "elements.js",
6
6
  "type": "module",