@epa-wg/custom-element 0.0.21 → 0.0.23

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
@@ -348,9 +348,9 @@ within template
348
348
  [github-image]: https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/mark-github.svg
349
349
  [npm-image]: https://img.shields.io/npm/v/@epa-wg/custom-element.svg
350
350
  [npm-url]: https://npmjs.org/package/@epa-wg/custom-element
351
- [coverage-image]: https://unpkg.com/@epa-wg/custom-element-dist@0.0.21/coverage/src/custom-element/coverage.svg
352
- [coverage-url]: https://unpkg.com/@epa-wg/custom-element-dist@0.0.21/coverage/src/custom-element/index.html
353
- [storybook-url]: https://unpkg.com/@epa-wg/custom-element-dist@0.0.21/storybook-static/index.html?path=/story/welcome--introduction
351
+ [coverage-image]: https://unpkg.com/@epa-wg/custom-element-dist@0.0.23/coverage/src/custom-element/coverage.svg
352
+ [coverage-url]: https://unpkg.com/@epa-wg/custom-element-dist@0.0.23/coverage/src/custom-element/index.html
353
+ [storybook-url]: https://unpkg.com/@epa-wg/custom-element-dist@0.0.23/storybook-static/index.html?path=/story/welcome--introduction
354
354
  [sandbox-url]: https://stackblitz.com/github/EPA-WG/custom-element?file=index.html
355
355
  [webcomponents-url]: https://www.webcomponents.org/element/@epa-wg/custom-element
356
356
  [webcomponents-img]: https://img.shields.io/badge/webcomponents.org-published-blue.svg
@@ -116,7 +116,7 @@ writeFileSync( '.././ide/customData-xsl.json', JSON.stringify( vsCode, undefined
116
116
  const intelliJ = {
117
117
  "$schema": "http://json.schemastore.org/web-types",
118
118
  "name": "@epa-wg/custom-element",
119
- "version": "0.0.21",
119
+ "version": "0.0.23",
120
120
  "js-types-syntax": "typescript",
121
121
  "description-markup": "markdown",
122
122
  "contributions": {
@@ -1,5 +1,9 @@
1
1
  export function log(x: any): void;
2
2
  export function deepEqual(a: any, b:any): boolean|0;
3
+ export function xml2dom(xmlString:string): Document;
4
+ export function xmlString(doc:Node|Document): string;
5
+ export function obj2node(o:any, tag:string, doc:Document): HTMLElement;
6
+ export function tagUid(node:HTMLElement): HTMLElement;
3
7
 
4
8
  /**
5
9
  * @summary Declarative Custom Element as W3C proposal PoC with native(XSLT) based templating
package/custom-element.js CHANGED
@@ -13,7 +13,6 @@ const attr = (el, attr)=> el.getAttribute?.(attr)
13
13
  , createText = ( d, t) => (d.ownerDocument || d ).createTextNode( t )
14
14
  , removeChildren = n => { while(n.firstChild) n.firstChild.remove(); return n; }
15
15
  , emptyNode = n => { n.getAttributeNames().map( a => n.removeAttribute(a) ); return removeChildren(n); }
16
- , createNS = ( ns, tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElementNS( ns, tag ))
17
16
  , xslNs = x => ( x?.setAttribute('xmlns:xsl', XSL_NS_URL ), x )
18
17
  , xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(x) )
19
18
  , cloneAs = (p,tag) =>
@@ -23,7 +22,7 @@ const attr = (el, attr)=> el.getAttribute?.(attr)
23
22
  while( p.firstChild )
24
23
  px.append(p.firstChild);
25
24
  return px;
26
- }
25
+ };
27
26
 
28
27
  function
29
28
  ASSERT(x)
@@ -66,17 +65,22 @@ assureSlot( e )
66
65
  export function
67
66
  obj2node( o, tag, doc )
68
67
  { const t = typeof o;
69
- if( t === 'function'){debugger}
70
68
  if( t === 'string' )
71
69
  return create(tag,o,doc);
72
70
  if( t === 'number' )
73
71
  return create(tag,''+o,doc);
74
72
 
75
73
  if( o instanceof Array )
76
- { const ret = create('array');
74
+ { const ret = create('array','',doc);
77
75
  o.map( ae => ret.append( obj2node(ae,tag,doc)) );
78
76
  return ret
79
77
  }
78
+ if( o instanceof FormData )
79
+ { const ret = create('form-data','',doc);
80
+ for( const p of o )
81
+ ret.append( obj2node(p[1],p[0],doc) );
82
+ return ret
83
+ }
80
84
  const ret = create(tag,'',doc);
81
85
  for( let k in o )
82
86
  if( isNode(o[k]) || typeof o[k] ==='function' || o[k] instanceof Window )
@@ -91,13 +95,14 @@ obj2node( o, tag, doc )
91
95
  export function
92
96
  tagUid( node )
93
97
  { // {} to xsl:value-of
94
- forEach$(node,'*',d => [...d.childNodes].filter( e=>e.nodeType === 3 ).forEach( e=>
95
- { if( e.parentNode.localName === 'style' )
96
- return;
97
- const m = e.data.matchAll( /{([^}]*)}/g );
98
+ forEach$(node,'*',d => [...d.childNodes]
99
+ .filter( e => e.nodeType === 3 && e.parentNode.localName !== 'style' && e.data )
100
+ .forEach( e=>
101
+ { const s = e.data,
102
+ m = s.matchAll( /{([^}]*)}/g );
98
103
  if(m)
99
104
  { let l = 0
100
- , txt = t => createText(e,t||'')
105
+ , txt = t => createText(e,t)
101
106
  , tt = [];
102
107
  [...m].forEach(t=>
103
108
  { if( t.index > l )
@@ -107,8 +112,8 @@ tagUid( node )
107
112
  tt.push(v);
108
113
  l = t.index+t[0].length;
109
114
  })
110
- if( l < e.data.length)
111
- tt.push( txt( e.data.substring(l,e.data.length) ));
115
+ if( l < s.length)
116
+ tt.push( txt( s.substring(l,s.length) ));
112
117
  if( tt.length )
113
118
  { for( let t of tt )
114
119
  d.insertBefore(t,e);
@@ -250,7 +255,7 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
250
255
  const slotCall = $(xslDom,'call-template[name="slot"]')
251
256
  , slot2xsl = s =>
252
257
  { const v = slotCall.cloneNode(true)
253
- , name = attr(s,'name') || '';
258
+ , name = attr(s,'name');
254
259
  name && v.firstElementChild.setAttribute('select',`'${ name }'`)
255
260
  for( let c of s.childNodes)
256
261
  v.lastElementChild.append(c)
@@ -297,16 +302,20 @@ deepEqual(a, b, O=false)
297
302
  return O
298
303
  return true;
299
304
  }
305
+ const splitSliceNames = v => v.split('|').map( s=>s.trim() ).filter(s=>s);
306
+
300
307
  export const
301
308
  assureSlices = ( root, names) =>
302
- names.split('|').map(n=>n.trim()).map( xp =>
303
- { if(xp.includes('/'))
304
- { const ret = [], r = root.ownerDocument.evaluate( xp, root );
309
+ splitSliceNames(names).map( xp =>
310
+ { let d = root.ownerDocument
311
+ , append = n=> (root.append(n),n);
312
+ if(xp.includes('/'))
313
+ { const ret = [], r = d.evaluate( xp, root );
305
314
  for( let n; n = r.iterateNext(); )
306
315
  ret.push( n )
307
316
  return ret
308
317
  }
309
- return [...root.childNodes].find(n=>n.localName === xp) || create(xp);
318
+ return [...root.childNodes].find(n=>n.localName === xp) || append( create(xp,'',d) );
310
319
  }).flat();
311
320
 
312
321
  /**
@@ -319,19 +328,24 @@ assureSlices = ( root, names) =>
319
328
  export function
320
329
  event2slice( x, sliceNames, ev, dce )
321
330
  {
331
+ if( ev.sliceProcessed )
332
+ return
333
+ ev.sliceProcessed = 1;
322
334
  // evaluate slices[]
323
335
  // inject @attributes
324
336
  // inject event
325
337
  // evaluate slice-value
326
338
  // slice[i] = slice-value
327
- assureSlices(x,sliceNames).map( s =>
339
+ return assureSlices( x, sliceNames ?? '' ).map( s =>
328
340
  {
329
341
  const d = x.ownerDocument
330
342
  , el = ev.sliceEventSource
331
343
  , sel = ev.sliceElement
332
- , cleanSliceValue = ()=>[...s.childNodes].filter(n=>n.nodeType===3 || n.localName==='value').map(n=>n.remove());
344
+ , cleanSliceValue = ()=>[...s.childNodes].filter(n=>n.nodeType===3 || n.localName==='value' || n.localName==='form-data').map(n=>n.remove());
333
345
  el.getAttributeNames().map( a => s.setAttribute( a, attr(el,a) ) );
334
346
  [...s.childNodes].filter(n=>n.localName==='event').map(n=>n.remove());
347
+ if( 'validationMessage' in el )
348
+ s.setAttribute('validation-message', el.validationMessage);
335
349
  ev.type==='init' && cleanSliceValue();
336
350
  s.append( obj2node( ev, 'event', d ) );
337
351
  if( sel.hasAttribute('slice-value') )
@@ -343,7 +357,12 @@ event2slice( x, sliceNames, ev, dce )
343
357
  cleanSliceValue();
344
358
  s.append( createText( d, v ) );
345
359
  }else
346
- { const v = el.value ?? attr( sel, 'value' ) ;
360
+ { if( 'elements' in el )
361
+ { cleanSliceValue();
362
+ s.append( obj2node(new FormData(el),'value', s.ownerDocument) )
363
+ return s
364
+ }
365
+ const v = el.value ?? attr( sel, 'value' ) ;
347
366
  cleanSliceValue();
348
367
  if( v === null || v === undefined )
349
368
  [...s.childNodes].filter(n=>n.localName!=='event').map(n=>n.remove());
@@ -353,6 +372,7 @@ event2slice( x, sliceNames, ev, dce )
353
372
  else
354
373
  s.append( obj2node(v,'value',s.ownerDocument) )
355
374
  }
375
+ return s
356
376
  })
357
377
  }
358
378
 
@@ -360,7 +380,6 @@ function forEach$( el, css, cb){
360
380
  if( el.querySelectorAll )
361
381
  [...el.querySelectorAll(css)].forEach(cb)
362
382
  }
363
- const getByHashId = ( n, id )=> ( p => n===p? null: (p && ( p.querySelector(id) || getByHashId(p,id) ) ))( n.getRootNode() )
364
383
  const loadTemplateRoots = async ( src, dce )=>
365
384
  {
366
385
  if( !src || !src.trim() )
@@ -388,12 +407,7 @@ const loadTemplateRoots = async ( src, dce )=>
388
407
  }catch (error){ return [dce]}
389
408
  }
390
409
  export function mergeAttr( from, to )
391
- { if( isText(from) )
392
- {
393
- if( !isText(to) ){ debugger }
394
- return
395
- }
396
- for( let a of from.attributes)
410
+ { for( let a of from.attributes)
397
411
  { a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
398
412
  if( a.name === 'value')
399
413
  to.value = a.value
@@ -472,12 +486,18 @@ export const xPathDefaults = x=>
472
486
  // return xx.length ? `${a}|(${xPathDefaults(xx.join('??'))})[not(${a})]`: a
473
487
  }
474
488
  export const xPath = (x,root)=>
475
- { x = xPathDefaults(x);
489
+ {
490
+ const xx = x.split('??');
491
+ if( xx.length > 1 )
492
+ return xPath(xx[0], root) || xPath(xx[1], root);
493
+
494
+ x = xPathDefaults(x);
476
495
 
477
496
  const it = root.ownerDocument.evaluate(x, root);
478
497
  switch( it.resultType )
479
498
  { case XPathResult.NUMBER_TYPE: return it.numberValue;
480
499
  case XPathResult.STRING_TYPE: return it.stringValue;
500
+ case XPathResult.BOOLEAN_TYPE: return it.booleanValue;
481
501
  }
482
502
 
483
503
  let ret = '';
@@ -529,7 +549,10 @@ CustomElement extends HTMLElement
529
549
 
530
550
  const dce = this
531
551
  , sliceNodes = [...this.templateNode.querySelectorAll('[slice]')]
532
- , sliceNames = sliceNodes.map(e=>attr(e,'slice')).filter(n=>!n.includes('/')).filter((v, i, a)=>a.indexOf(v) === i)
552
+ , sliceNames = sliceNodes.map(e=>attr(e,'slice'))
553
+ .filter(n=>!n.includes('/'))
554
+ .filter((v, i, a)=>a.indexOf(v) === i)
555
+ .map(splitSliceNames).flat()
533
556
  , declaredAttributes = templateDocs.reduce( (ret,t) => { if( t.params ) ret.push( ...t.params ); return ret; }, [] );
534
557
 
535
558
  class DceElement extends HTMLElement
@@ -540,6 +563,8 @@ CustomElement extends HTMLElement
540
563
  { let payload = this.childNodes;
541
564
  if( this.firstElementChild?.tagName === 'TEMPLATE' )
542
565
  {
566
+ if( this.firstElementChild !== this.lastElementChild )
567
+ { console.error('payload should have TEMPLATE as only child', this.outerHTML ) }
543
568
  const t = this.firstElementChild;
544
569
  t.remove();
545
570
  payload = t.content.childNodes;
@@ -563,7 +588,7 @@ CustomElement extends HTMLElement
563
588
  })(x.ownerDocument.createElement( tag ))
564
589
  injectData( x, 'payload' , payload , assureSlot );
565
590
  this.innerHTML='';
566
- injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
591
+ const attrsRoot = injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
567
592
  injectData( x, 'dataset', Object.keys( this.dataset ), k => createXmlNode( k, this.dataset[ k ] ) );
568
593
  const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) )
569
594
  , sliceXPath = x => xPath(x, sliceRoot);
@@ -585,14 +610,12 @@ CustomElement extends HTMLElement
585
610
  let timeoutID;
586
611
 
587
612
  this.onSlice = ev=>
588
- { ev.stopPropagation?.();
589
- ev.sliceEventSource = ev.currentTarget || ev.target;
590
- sliceEvents.push(ev);
613
+ { sliceEvents.push(ev);
591
614
  if( !timeoutID )
592
615
  timeoutID = setTimeout(()=>
593
616
  { applySlices();
594
617
  timeoutID =0;
595
- },10);
618
+ },1);
596
619
  };
597
620
  const transform = this.transform = ()=>
598
621
  { if(this.#inTransform){ debugger }
@@ -619,20 +642,58 @@ CustomElement extends HTMLElement
619
642
  }
620
643
  })
621
644
 
622
- forEach$( this,'[slice]', el =>
645
+ forEach$( this,'[slice],[slice-event]', el =>
623
646
  { if( !el.dceInitialized )
624
647
  { el.dceInitialized = 1;
625
- const evs = attr(el,'slice-event');
626
- (evs || 'change')
627
- .split(' ')
648
+ let evs = attr(el,'slice-event');
649
+ if( attr(el,'custom-validity') )
650
+ evs += ' change submit';
651
+
652
+ [...new Set((evs || 'change') .split(' '))]
628
653
  .forEach( t=> (el.localName==='slice'? el.parentElement : el)
629
- .addEventListener( t, ev=>
630
- { ev.sliceElement = el;
631
- this.onSlice(ev)
632
- } ));
654
+ .addEventListener( t, ev=>
655
+ { ev.sliceElement = el;
656
+ ev.sliceEventSource = ev.currentTarget || ev.target;
657
+ const slices = event2slice( sliceRoot, attr( ev.sliceElement, 'slice'), ev, this );
658
+
659
+ forEach$(this,'[custom-validity]',el =>
660
+ { if( !el.setCustomValidity )
661
+ return;
662
+ const x = attr( el, 'custom-validity' );
663
+ try
664
+ { const v = x && xPath( x, attrsRoot );
665
+ el.setCustomValidity( v === true? '': v === false ? 'invalid' : v );
666
+ }catch(err)
667
+ { console.error(err, 'xPath', x) }
668
+ })
669
+ const x = attr(el,'custom-validity')
670
+ , v = x && xPath( x, attrsRoot )
671
+ , msg = v === true? '' : v;
672
+
673
+ if( x )
674
+ { el.setCustomValidity ? el.setCustomValidity( msg ) : ( el.validationMessage = msg );
675
+ slices.map( s => s.setAttribute('validation-message', msg ) );
676
+ if( ev.type === 'submit' )
677
+ { if( v === true )
678
+ return;
679
+ setTimeout(transform,1)
680
+ if( !!v === v )
681
+ { v || ev.preventDefault();
682
+ return v;
683
+ }
684
+ if( v )
685
+ { ev.preventDefault();
686
+ return !1
687
+ }
688
+ return ;
689
+ }else
690
+ setTimeout(transform,1)
691
+ }
692
+ this.onSlice(ev);
693
+ } ));
633
694
  if( !evs || evs.includes('init') )
634
695
  { if( el.hasAttribute('slice-value') || el.hasAttribute('value') || el.value )
635
- this.onSlice({type:'init', target: el, sliceElement:el })
696
+ this.onSlice({type:'init', target: el, sliceElement:el, sliceEventSource:el })
636
697
  else
637
698
  el.value = sliceXPath( attr(el,'slice') )
638
699
  }
package/demo/a.html CHANGED
@@ -1,63 +1,60 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xhtml="http://www.w3.org/1999/xhtml"
3
- xmlns:html="http://www.w3.org/1999/xhtml">
2
+ <html lang="en" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xhtml="http://www.w3.org/1999/xhtml">
4
3
  <head>
5
4
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
- <title>custom-element Declarative Custom Element implementation demo</title>
5
+ <title>Data slices - Declarative Custom Element implementation demo</title>
7
6
  <link rel="icon" href="./wc-square.svg"/>
7
+
8
8
  <script type="module" src="../http-request.js"></script>
9
- <script type="module" src="../local-storage.js"></script>
10
9
  <script type="module" src="../custom-element.js"></script>
11
-
12
10
  <style>
13
11
  @import "./demo.css";
14
12
 
15
- button {
16
- background: forestgreen;
17
- }
18
-
19
- table {
20
- min-width: 16rem;
21
- }
22
-
23
- td {
24
- border-bottom: 1px solid silver;
25
- }
26
-
27
- tfoot td {
28
- border-bottom: none;
13
+ label {
14
+ display: flex;
29
15
  }
30
16
 
31
- td, th {
32
- text-align: right;
17
+ label:has(input[type="text"],input[type="password"],input:not([type]) ) {
18
+ flex-direction: column;
33
19
  }
34
20
 
35
- caption {
36
- padding: 1rem;
37
- font-weight: bolder;
38
- font-family: sans-serif;
21
+ nav {
22
+ max-width: 32em;
39
23
  }
40
24
  </style>
25
+ <!-- https://github.com/mdn/learning-area/blob/main/html/forms/form-validation/custom-error-message.html
26
+ todo: apply setCustomValidity( warningStr )
27
+ -->
28
+
41
29
  </head>
42
30
  <body>
43
31
 
44
-
45
- <custom-element tag="dce-1">
46
- <template >
47
- <style >
48
- color:red;
49
- </style>
50
-
51
- <u ><slot>red</slot></u>
32
+ <fieldset>
33
+ <legend><b style="color:green">green</b> in instance style can be overridden in payload as <i
34
+ style="color:red">red</i> in 1st instance
35
+ </legend>
36
+ <custom-element tag="dce-3">
37
+ <template>
38
+ <u>
39
+ <slot>is green</slot>
40
+ </u>
52
41
  </template>
42
+ <style>dce-3 {
43
+ color: green
44
+ }</style>
53
45
  </custom-element>
54
- <dce-1></dce-1> *
55
- <dce-1><template >
56
- <style >
57
- color:green;
58
- </style>
59
-
60
- <u >green</u>
61
- </template></dce-1>
46
+ <u>should be</u> <i style="color:red">red</i>:
47
+ <dce-3 id="dce32">
48
+ <template>
49
+ <style> color:red; </style>
50
+ <u>red</u>
51
+ </template>
52
+ </dce-3> <br/>
53
+ should be GREEN:
54
+ <dce-3 id="dce31">green</dce-3>
55
+ </fieldset>
56
+
57
+ <script type="module" src="https://unpkg.com/html-demo-element@1/html-demo-element.js"></script>
58
+
62
59
  </body>
63
60
  </html>
package/demo/b.html ADDED
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <dce-root data-dce-id="1" xmlns="http://www.w3.org/1999/xhtml" xmlns:dce="urn:schemas-epa-wg:dce"
3
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"><u data-dce-id="2" xmlns="">
4
+ <dce-text data-dce-id="3">
5
+ <xhtml:span xmlns:xsl="http://www.w3.org/1999/XSL/Transform" slot=""/>
6
+ <xhtml:style xmlns:xsl="http://www.w3.org/1999/XSL/Transform" slot="" title="ABC">
7
+ dce-3[data-dce-style="54f96d52-ce70-435d-83c4-b421357d9a17"]{ color:red; }
8
+ </xhtml:style>
9
+ <xhtml:span xmlns:xsl="http://www.w3.org/1999/XSL/Transform" slot=""/>
10
+ <xhtml:u xmlns:xsl="http://www.w3.org/1999/XSL/Transform" slot="">red</xhtml:u>
11
+ <xhtml:span xmlns:xsl="http://www.w3.org/1999/XSL/Transform" slot=""/>
12
+ </dce-text>
13
+ </u></dce-root>
@@ -178,6 +178,38 @@
178
178
  </template>
179
179
  </html-demo-element>
180
180
 
181
+
182
+ <html-demo-element legend="10. multiple slices by same field"
183
+ description="same element value sets s1 and s2 slice">
184
+ <template>
185
+ <custom-element>
186
+ <template>
187
+ <input slice="s1|s2"
188
+ slice-event="input"
189
+ data-testid="f1"
190
+ /><br/>
191
+ Type to update s1 and s2 slices <br/>
192
+ slice <code>s1: {//slice/s1}</code><br/>
193
+ slice <code>s2: {//slice/s2}</code><br/>
194
+ </template>
195
+ </custom-element>
196
+ </template>
197
+ </html-demo-element>
198
+
199
+ <html-demo-element legend="11. slices and attribute"
200
+ description="initial attribute value should be smile as emoji and :) on blur from input it should be updated from value">
201
+ <template>
202
+ <custom-element>
203
+ <template>
204
+ <attribute name="emotion">😃</attribute>
205
+ <input slice="/datadom/attributes/emotion | s1"/>
206
+ Type and unfocus to update emotion attribute: {emotion}
207
+ and slice: {//slice/s1}
208
+ </template>
209
+ </custom-element>
210
+ </template>
211
+ </html-demo-element>
212
+
181
213
  <script type="module" src="https://unpkg.com/html-demo-element@1/html-demo-element.js"></script>
182
214
 
183
215
  </body>
package/demo/form.html ADDED
@@ -0,0 +1,240 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xhtml="http://www.w3.org/1999/xhtml">
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5
+ <title>Forms - Declarative Custom Element implementation demo</title>
6
+ <link rel="icon" href="./wc-square.svg"/>
7
+
8
+ <script type="module" src="../local-storage.js"></script>
9
+ <script type="module" src="../custom-element.js"></script>
10
+ <style>
11
+ @import "./demo.css";
12
+
13
+ label {
14
+ display: flex;
15
+ }
16
+
17
+ label:has(input[type="text"],input[type="password"],input:not([type]) ) {
18
+ flex-direction: column;
19
+ }
20
+
21
+ nav {
22
+ max-width: 32em;
23
+ }
24
+ </style>
25
+ <!-- https://github.com/mdn/learning-area/blob/main/html/forms/form-validation/custom-error-message.html
26
+ todo: apply setCustomValidity( warningStr )
27
+ -->
28
+
29
+ </head>
30
+ <body>
31
+
32
+ <nav>
33
+ <a href="../index.html"><h3><code>custom-element</code> demo</h3></a>
34
+
35
+ <p> <b>formData</b><br/>
36
+ The values of named form fields are populated into the <b>slice</b> as <b>form-data</b> on <var>change</var>
37
+ or <var>submit</var> event. The field values can be used in form validation via <var>custom-validity</var>
38
+ attribute
39
+ and in condition to enabling the form parts
40
+ <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData">formData MDN</a>
41
+ </p>
42
+ <details>
43
+ <summary>slice to form-data mapping</summary>
44
+ <html-demo-element>
45
+ <template>
46
+ <datadom hidden>
47
+ <slice>
48
+ <signin-form>
49
+ <form-data>
50
+ <username>QWE</username>
51
+ <password>ASD</password>
52
+ </form-data>
53
+ </signin-form>
54
+ </slice>
55
+ </datadom>
56
+ </template>
57
+ </html-demo-element>
58
+ </details>
59
+
60
+
61
+ <p> <b> custom-validity attribute </b><br/>
62
+ applied on the form itself or on the form field.<br/>
63
+ The value is an XPath over DCE <var>datadom</var>. When evaluated as <u>boolean</u>, it would enable(true) or
64
+ disable(false)
65
+ the form submission, acting as form validation mechanism.<br>
66
+ Alternatively, the value can be evaluated as a <u>string</u> which would be treated as an error and can be used
67
+ as
68
+ validation error message set as <var>@validation-message</var> attribute on the form slice.
69
+ Look for <var>email-form/@validation-message</var> example on the page.<br/>
70
+ </p>
71
+ <p> When <var>custom-validity</var> attribute is set on the field, its XPath evaluated value is propagated to
72
+ <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validationMessage">
73
+ validationMessage property</a>. Which would be shown via browser system popup as the field validation error.
74
+ </p>
75
+ <p> <var>@validation-message</var> is set either by <var>custom-validity</var> attribute or by browser as system message.
76
+ By default, it is shown as popup on the field validation. But also is available for template as a string via
77
+ form field attribute. Like in <var>email-form/@validation-message</var>.
78
+
79
+ </p>
80
+
81
+ <a href="https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation">Form Validation MDN</a>
82
+ </nav>
83
+
84
+ <html-demo-element legend="1. Simple validation"
85
+ description="custom-validity boolean value prevents submission, username length switches Next to 'Sign In' button ">
86
+ <ol>
87
+ <li> Click Next, observe the warning</li>
88
+ <li> Fill input with 10+ characters</li>
89
+ <li> Click Next, the password and "Sign In" button should appear</li>
90
+ </ol>
91
+ <template>
92
+ <custom-element>
93
+ <template>
94
+ <form slice="signin-form"
95
+ custom-validity="
96
+ string-length(/datadom/slice/signin-form/form-data/username) &gt; 10
97
+ and string-length(//form-data/password) &gt; 3 "
98
+ >
99
+ <label> Email
100
+ <input name="username" autocomplete="username" placeholder="Email, phone, or username"
101
+ required="">
102
+ </label>
103
+ <variable name="showpassword" select="string-length(//form-data/username) &gt; 10 "></variable>
104
+ <if test="not($showpassword)">
105
+ <button slice="confirm" slice-event="click" slice-value="'password'">Next</button>
106
+ </if>
107
+ <if test="$showpassword">
108
+ <label>Enter password: <input name="password" type="password" required> </label>
109
+ <button>Sign In</button>
110
+ </if>
111
+ username {//username}
112
+ </form>
113
+ </template>
114
+ </custom-element>
115
+ </template>
116
+ </html-demo-element>
117
+
118
+ <html-demo-element legend="2. Form life cycle demo"
119
+ description="form-data in the form slice is the source of truth">
120
+
121
+ <template>
122
+ <custom-element>
123
+ <template>
124
+ <form slice="signin-form"
125
+ custom-validity="
126
+ string-length(/datadom/slice/signin-form/form-data/username) &gt; 9
127
+ and ( ( //confirm-by = 'sms' )
128
+ or ( //confirm-by = 'email' )
129
+ or ( //confirm-by = 'password' and string-length(//form-data/password) &gt; 3 )
130
+ )
131
+ "
132
+ >
133
+ <!-- form validity should be based on form-data -->
134
+ <variable name="warn">
135
+ <if test="string-length(//username-slice) &lt; 9 ">
136
+ Should be 10 or more symbols. &nbsp;
137
+ <!-- updated by slice on input event -->
138
+ </if>
139
+ <if test="//form-data/confirm-by = 'sms'">
140
+ Message and Data Rates may apply.
141
+ <!-- updated by form change by radio select -->
142
+ </if>
143
+ </variable>
144
+ <label> Enter your email, phone, or user name
145
+ <input name="username" autocomplete="username"
146
+ placeholder="Email, phone, or username"
147
+ custom-validity="( string-length(//username-slice) &gt; 9 ) ?? 'should be 10+ symbols'"
148
+ slice-event="input"
149
+ slice="username-slice"
150
+ required
151
+ />
152
+ </label>
153
+ <var> {$warn} </var>
154
+ <fieldset>
155
+ <legend>Confirm by</legend>
156
+ <label><input type="radio" name="confirm-by" value="email"/> email </label>
157
+ <label><input type="radio" name="confirm-by" value="sms"/> text to phone </label>
158
+ <label><input type="radio" name="confirm-by" value="password"/> password </label>
159
+ <if test="/datadom/slice/signin-form/form-data/confirm-by = 'password'">
160
+ <label>Enter password: <input type="password" NAME="password"
161
+ custom-validity="( string-length(//form-data/password) &gt; 3 ) ?? 'password is too short'"
162
+ /></label>
163
+ </if>
164
+ <if test="not(//confirm-by)">
165
+ Please select the auth method
166
+ </if>
167
+ </fieldset>
168
+ <section>
169
+ <button>Sign In</button>
170
+ </section>
171
+ </form>
172
+ //username-slice {//username-slice}<br/>
173
+ //username {//username}<br/>
174
+ //confirm-by {//confirm-by}<br/>
175
+ //password {//password}
176
+ </template>
177
+ </custom-element>
178
+ </template>
179
+ </html-demo-element>
180
+
181
+ <html-demo-element legend="3. read system validity message"
182
+ description="validationMessage propagated into slice as 'validation-message' attribute ">
183
+ <ol>
184
+ <li> type in input field</li>
185
+ <li> delete input field content</li>
186
+ <li> observe the warning in string bellow input</li>
187
+ <li> Click Next observe the system warning in dropdown and in string bellow input</li>
188
+ </ol>
189
+ <template>
190
+ <custom-element>
191
+ <template>
192
+ <form slice="email-form">
193
+ <label> Email
194
+ <input slice="username" slice-event="input" placeholder="non-empty" required>
195
+ </label>
196
+ <if test="//username/@validation-message">
197
+ <var>{//username/@validation-message}</var>
198
+ </if>
199
+ <button>Next</button>
200
+ <p>{//email-form/@validation-message}</p>
201
+ </form>
202
+ </template>
203
+ </custom-element>
204
+ </template>
205
+ </html-demo-element>
206
+
207
+ <html-demo-element legend="4. form validity message"
208
+ description="@validation-message propagated into form slice and ">
209
+ <ol>
210
+ <li> type up to 3 chars in input field</li>
211
+ <li> observe the slice value change</li>
212
+ <li> click next</li>
213
+ <li> observe the warning bellow the button</li>
214
+ </ol>
215
+ <template>
216
+ <custom-element>
217
+ <template>
218
+ <form slice="email-form"
219
+ custom-validity=" string-length(//slice/username) &gt; 3 ??
220
+ concat('should be more than 3 characters, now is ',string-length(//slice/username) ) "
221
+ >
222
+ <label> Email
223
+ <input name="email" slice="username" slice-event="input" placeholder="non-empty" required/>
224
+ </label>
225
+ <if test="//username/@validation-message">
226
+ <var>{//username/@validation-message}</var>
227
+ </if>
228
+ <button>Next</button>
229
+ <p>//email-form/@validation-message: {//email-form/@validation-message} </p>
230
+ <p>//slice/username: {//slice/username} </p>
231
+ </form>
232
+ </template>
233
+ </custom-element>
234
+ </template>
235
+ </html-demo-element>
236
+
237
+ <script type="module" src="https://unpkg.com/html-demo-element@1/html-demo-element.js"></script>
238
+
239
+ </body>
240
+ </html>
package/demo/s.xml CHANGED
@@ -1,14 +1,11 @@
1
- <div xmlns="http://www.w3.org/1999/xhtml" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><!-- wrapping into template to prevent images loading within DCE declaration -->
2
- <local-storage key="undefined" slice="s" }=""></local-storage>
3
-
4
- <input placeholder="value for localStorage" slice="s" value="{ //s ?? 'undefined' }" />
5
- <button>set</button>
6
- <button slice="sv" slice-value="''" slice-event="click">set blank</button>
7
- <button slice="sv" slice-value="'/reflect'" slice-event="click">/reflect</button>
8
- <button slice="sv" slice-value="'/pokemon?limit=6'" slice-event="click">/pokemon?limit=6</button>
9
- <button slice="sv" slice-value="'/pokemon?limit=3'" slice-event="click">/pokemon?limit=3</button>
10
- <br />
11
- <var>undefined</var>:<code>{ //slice/s }</code>
12
- <br />
13
- undefined
14
- </div>
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <datadom>
3
+ <slice>
4
+ <signin-form>
5
+ <form-data>
6
+ <username>QWE</username>
7
+ <password>ASD</password>
8
+ </form-data>
9
+ </signin-form>
10
+ </slice>
11
+ </datadom>
package/demo/s.xslt CHANGED
@@ -1,3 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
1
2
  <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xhtml="http://www.w3.org/1999/xhtml"
2
3
  xmlns:dce="urn:schemas-epa-wg:dce" xmlns:exsl="http://exslt.org/common" version="1.0"
3
4
  exclude-result-prefixes="exsl">
@@ -14,56 +15,39 @@
14
15
  </xsl:template>
15
16
  <xsl:template mode="payload" match="attributes">
16
17
  <dce-root xmlns="http://www.w3.org/1999/xhtml" xmlns:xhtml="http://www.w3.org/1999/xhtml" data-dce-id="1">
17
- <http-request xmlns="" url="{concat(//s , '') }" slice="s" data-dce-id="2"/>
18
- <p xmlns="" data-dce-id="3">Pokemon Count:
19
- <xsl:value-of select="count(/datadom/slice/s//results)"/>
20
- </p>
21
- <xsl:if xmlns:xsl="http://www.w3.org/1999/XSL/Transform" test="count(/datadom/slice/s//results) &lt; 0">
22
- <h3 xmlns="" data-dce-id="4">loading...</h3>
23
- </xsl:if>
24
- <xsl:for-each xmlns:xsl="http://www.w3.org/1999/XSL/Transform" select="/datadom/slice/s//results">
25
- <xsl:variable name="pokeid"
26
- select="substring-before( substring-after( @url, 'https://pokeapi.co/api/v2/pokemon/'),'/')"/>
27
- <button xmlns="" data-dce-id="5">
28
- <xsl:value-of select="@name">
29
- </xsl:value-of>
30
- </button>
31
- </xsl:for-each>
32
- <xsl:for-each xmlns:xsl="http://www.w3.org/1999/XSL/Transform" select="//slice/s/value/*">
33
- <ul xmlns="" data-dce-id="6">
34
- <var data-testid="request-section" data-dce-id="7">
35
- <dce-text data-dce-id="8">
36
- <xsl:value-of select="name(.)"/>
37
- </dce-text>
38
- </var>
39
- <xsl:for-each select="@*">
40
- <div data-dce-id="9">
41
- <var data-dce-id="10">@<xsl:value-of select="local-name(.)"/>
42
- </var>
43
- <dce-text data-dce-id="11">
44
- =
45
- </dce-text>
46
- <code data-testid="attr-{local-name(.)}" data-dce-id="12">
47
- <xsl:value-of select="."/>
48
- </code>
49
- </div>
50
- </xsl:for-each>
51
- </ul>
52
- </xsl:for-each>
18
+ <u xmlns="" data-dce-id="2">
19
+ <dce-text data-dce-id="3">
20
+ <xsl:call-template name="slot">
21
+ <xsl:with-param name="slotname" select="''"/>
22
+ <xsl:with-param name="defaultvalue">
23
+ <dce-text xmlns="" data-dce-id="4">is green</dce-text>
24
+ </xsl:with-param>
25
+ </xsl:call-template>
26
+ </dce-text>
27
+ </u>
53
28
  </dce-root>
54
29
  </xsl:template>
55
30
  <xsl:template match="/">
56
31
  <xsl:apply-templates mode="payload" select="/datadom/attributes"/>
57
32
  </xsl:template>
33
+
34
+ <xsl:template match="@*|node()" mode="copy-html">
35
+ <xsl:copy><xsl:apply-templates select="@*|node()" mode="copy-html"/></xsl:copy>
36
+ </xsl:template>
37
+ <xsl:template match="node()[starts-with(name(),'xhtml:')]" mode="copy-html">
38
+ <xsl:element name="{local-name()}"><xsl:apply-templates select="@*|node()" mode="copy-html"/></xsl:element>
39
+ </xsl:template>
40
+
41
+
58
42
  <xsl:template name="slot">
59
43
  <xsl:param name="slotname"/>
60
44
  <xsl:param name="defaultvalue"/>
61
45
  <xsl:choose>
62
46
  <xsl:when test="//payload/*[@slot=$slotname]">
63
- <xsl:copy-of select="//payload/*[@slot=$slotname]"/>
47
+ <xsl:apply-templates mode="copy-html" select="//payload/*[@slot=$slotname]"/>
64
48
  </xsl:when>
65
49
  <xsl:otherwise>
66
- <xsl:copy-of select="$defaultvalue"/>
50
+ <xsl:apply-templates mode="copy-html" select="$defaultvalue"/>
67
51
  </xsl:otherwise>
68
52
  </xsl:choose>
69
53
  </xsl:template>
package/demo/s1.xslt ADDED
@@ -0,0 +1,60 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xhtml="http://www.w3.org/1999/xhtml"
3
+ xmlns:dce="urn:schemas-epa-wg:dce" xmlns:exsl="http://exslt.org/common" version="1.0"
4
+ exclude-result-prefixes="exsl">
5
+ <xsl:template match="ignore">
6
+ <xsl:choose>
7
+ <xsl:when test="//attr">
8
+ <xsl:value-of select="//attr"/>
9
+ </xsl:when>
10
+ <xsl:otherwise>
11
+ <xsl:value-of select="def"/>
12
+ </xsl:otherwise>
13
+ </xsl:choose>
14
+ <xsl:value-of select="."/>
15
+ </xsl:template>
16
+ <xsl:template mode="payload" match="attributes">
17
+ <dce-root xmlns="http://www.w3.org/1999/xhtml" xmlns:xhtml="http://www.w3.org/1999/xhtml" data-dce-id="1">
18
+ <u xmlns="" data-dce-id="2">
19
+ <dce-text data-dce-id="3">
20
+ <xsl:call-template name="slot">
21
+ <xsl:with-param name="slotname" select="''"/>
22
+ <xsl:with-param name="defaultvalue">
23
+ <dce-text xmlns="" data-dce-id="4">is green</dce-text>
24
+ </xsl:with-param>
25
+ </xsl:call-template>
26
+ </dce-text>
27
+ </u>
28
+ </dce-root>
29
+ </xsl:template>
30
+ <xsl:template match="/">
31
+ <xsl:apply-templates mode="payload" select="/datadom/attributes"/>
32
+ </xsl:template>
33
+
34
+ <xsl:template match="@*|node()" mode="copy-html">
35
+ <xsl:copy><xsl:apply-templates select="@*|node()" mode="copy-html"/></xsl:copy>
36
+ </xsl:template>
37
+ <xsl:template match="node()[starts-with(name(),'xhtml:')]" mode="copy-html">
38
+ <xsl:element name="{local-name()}"><xsl:apply-templates select="@*|node()" mode="copy-html"/></xsl:element>
39
+ </xsl:template>
40
+
41
+
42
+ <xsl:template name="slot">
43
+ <xsl:param name="slotname"/>
44
+ <xsl:param name="defaultvalue"/>
45
+ <xsl:choose>
46
+ <xsl:when test="//payload/*[@slot=$slotname]">
47
+ <xsl:apply-templates mode="copy-html" select="//payload/*[@slot=$slotname]"/>
48
+ </xsl:when>
49
+ <xsl:otherwise>
50
+ <xsl:apply-templates mode="copy-html" select="$defaultvalue"/>
51
+ </xsl:otherwise>
52
+ </xsl:choose>
53
+ </xsl:template>
54
+ <xsl:variable name="js-injected-body">
55
+ <xsl:call-template name="slot">
56
+ <xsl:with-param name="slotname" select="''"/>
57
+ <xsl:with-param name="defaultvalue"/>
58
+ </xsl:call-template>
59
+ </xsl:variable>
60
+ </xsl:stylesheet>
@@ -27,6 +27,19 @@
27
27
  }
28
28
  ]
29
29
  },
30
+ {
31
+ "name": "custom-validity",
32
+ "description": {
33
+ "kind": "markdown",
34
+ "value": "XPath expression to return either boolean or error string to be shown by browser native UI on form validation event. Unless value is true, prevents the form submission."
35
+ },
36
+ "references": [
37
+ {
38
+ "name": "Demo",
39
+ "url": "https://unpkg.com/@epa-wg/custom-element/demo/form.html"
40
+ }
41
+ ]
42
+ },
30
43
  {
31
44
  "name": "slice-value",
32
45
  "description": {
@@ -109,4 +122,4 @@
109
122
  ]
110
123
  }
111
124
  ]
112
- }
125
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json.schemastore.org/web-types",
3
3
  "name": "@epa-wg/custom-element",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "js-types-syntax": "typescript",
6
6
  "description-markup": "markdown",
7
7
  "contributions": {
@@ -17,6 +17,11 @@
17
17
  "description": "Defines the event name on which `value` would be synchronized with DCE slice\n\nOn: any component with `value` and associated change event",
18
18
  "doc-url": "https://unpkg.com/@epa-wg/custom-element/demo/dom-merge.html"
19
19
  },
20
+ {
21
+ "name": "custom-validity",
22
+ "description": "XPath expression to return either boolean or error string to be shown by browser native UI on form validation event. Unless value is true, prevents the form submission.",
23
+ "doc-url": "https://unpkg.com/@epa-wg/custom-element/demo/form.html"
24
+ },
20
25
  {
21
26
  "name": "slice-value",
22
27
  "description": "XPath expression to populate into the slice",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json.schemastore.org/web-types",
3
3
  "name": "@epa-wg/custom-element",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "js-types-syntax": "typescript",
6
6
  "description-markup": "markdown",
7
7
  "contributions": {
package/index.html CHANGED
@@ -35,6 +35,7 @@
35
35
  <a href="./demo/scoped-css.html" >scoped CSS </a> |
36
36
  <a href="./demo/parameters.html" >attributes </a> |
37
37
  <a href="./demo/data-slices.html" >data slices/events </a> |
38
+ <a href="./demo/form.html" >Form validation </a> |
38
39
  <a href="./demo/dom-merge.html" >DOM merge on dynamic update </a>
39
40
  </section>
40
41
  </nav>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epa-wg/custom-element",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Declarative Custom Element as W3C proposal PoC with native(XSLT) based templating",
5
5
  "browser": "custom-element.js",
6
6
  "module": "custom-element.js",