@epa-wg/custom-element 0.0.32 → 0.0.34

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/custom-element.js CHANGED
@@ -5,7 +5,7 @@ const XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform'
5
5
 
6
6
  // const log = x => console.debug( new XMLSerializer().serializeToString( x ) );
7
7
 
8
- const attr = (el, attr)=> el.getAttribute?.(attr)
8
+ const attr = (el, attr)=> el?.getAttribute?.(attr)
9
9
  , isText = e => e.nodeType === 3
10
10
  , isString = s => typeof s === 'string'
11
11
  , isNode = e => e && typeof e.nodeType === 'number'
@@ -89,7 +89,12 @@ obj2node( o, tag, doc )
89
89
  return create(tag,o,doc);
90
90
  if( t === 'number' )
91
91
  return create(tag,''+o,doc);
92
-
92
+ if( isNode(o) )
93
+ {
94
+ const el = create(tag);
95
+ el.append(o);
96
+ return el;
97
+ }
93
98
  if( o instanceof Array )
94
99
  { const ret = create('array','',doc);
95
100
  o.map( ae => ret.append( obj2node(ae,tag,doc)) );
@@ -103,13 +108,20 @@ obj2node( o, tag, doc )
103
108
  }
104
109
  const ret = create(tag,'',doc);
105
110
  for( let k in o )
106
- if( isNode(o[k]) || typeof o[k] ==='function' || o[k] instanceof Window )
111
+ {
112
+ if( typeof o[ k ] === 'function' || o[ k ] instanceof Window )
107
113
  continue
108
- else
109
- if( typeof o[k] !== "object" && isValidTagName(k) )
110
- ret.setAttribute(k, o[k] );
114
+ if( isNode( o[ k ] ) )
115
+ { if( k === 'data' || k==='value' )
116
+ ;
111
117
  else
112
- ret.append(obj2node(o[k], k, doc))
118
+ continue
119
+ }
120
+ if( typeof o[ k ] !== "object" && isValidTagName( k ) )
121
+ ret.setAttribute( k, o[ k ] )
122
+ else
123
+ ret.append( obj2node( o[ k ], k, doc ) )
124
+ }
113
125
  return ret;
114
126
  }
115
127
  export function
@@ -234,6 +246,8 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
234
246
  const d = xml2dom( '<xhtml/>' )
235
247
  , n = d.importNode(r, true);
236
248
  d.replaceChild(n,d.documentElement);
249
+ if( n.namespaceURI === HTML_NS_URL && !attr(n,'xmlns'))
250
+ n.setAttribute('xmlns',HTML_NS_URL);
237
251
  return xslHtmlNs(n);
238
252
  };
239
253
  if( e )
@@ -294,6 +308,16 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
294
308
  fr.append(r)
295
309
  }
296
310
 
311
+ [...fr.querySelectorAll('[test]')].forEach( n=>{
312
+ const t = attr(n,'test')
313
+ , r = t.replace(/hasBoolAttribute\((.*?)\)/g,
314
+ (match, p1, p2,p3,p4)=>
315
+ { const a = p1.substring(1);
316
+ return `(not($${a} = \'false\') and ($${a} = '' or $${a} = '${a}' or $${a} = 'true' ))`
317
+ });
318
+ t!== r && n.setAttribute('test',r);
319
+ });
320
+
297
321
  [...fr.querySelectorAll('dce-root>attribute')].forEach( a=>
298
322
  {
299
323
  keepAttributes(a,'namespace,name,select');
@@ -301,7 +325,7 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
301
325
  , name = attr(a,'name');
302
326
 
303
327
  declaredAttributes.push(name);
304
- if( a.childNodes.length)
328
+ if( a.childNodes.length )
305
329
  hardcodedAttributes[name] = a.textContent;
306
330
 
307
331
  payload.append(p);
@@ -317,10 +341,15 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
317
341
  if( select?.length>1 )
318
342
  { p.removeAttribute('select');
319
343
  const c = $( xslDom, 'template[match="ignore"]>choose').cloneNode(true);
320
- // todo multiple ?? operators
321
344
  emptyNode(c.firstElementChild).append( createText(c,'{'+select[0]+'}'));
322
- emptyNode(c.lastElementChild ).append( createText(c,'{'+select[1]+'}'));
323
- c.firstElementChild.setAttribute('test','string-length('+select[0]+')');
345
+ c.firstElementChild.setAttribute('test',select[0]);
346
+ for( let i=1; i<select.length-1; i++)
347
+ { const when = c.firstElementChild.cloneNode(true);
348
+ emptyNode(when).append( createText(c,'{'+select[i]+'}'));
349
+ when.setAttribute('test',select[i]);
350
+ c.insertBefore(when, c.lastElementChild);
351
+ }
352
+ emptyNode(c.lastElementChild ).append( createText(c,'{'+select[select.length-1]+'}'));
324
353
  p.append(c);
325
354
  val = c.cloneNode(true);
326
355
  }else
@@ -329,8 +358,12 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
329
358
  a.append(val);
330
359
  a.removeAttribute('select');
331
360
  }else
332
- { keepAttributes( p, 'name' );
361
+ {
362
+ keepAttributes( p, 'name' );
333
363
  p.setAttribute('select','/datadom/attributes/'+name)
364
+
365
+ if( !hardcodedAttributes[name] )
366
+ a.remove();
334
367
  }
335
368
  });
336
369
  [...fr.querySelectorAll('[value]')].filter(el=>el.getAttribute('value').match( /\{(.*)\?\?(.*)\}/g )).forEach(el=>
@@ -460,12 +493,15 @@ event2slice( x, sliceNames, ev, dce )
460
493
  const v = notChecked? '' : el.value ?? attr( el, 'value' );
461
494
  cleanSliceValue();
462
495
  if( v === null || v === undefined )
496
+ {
463
497
  [...s.childNodes].filter(n=>n.localName!=='event').map(n=>n.remove());
498
+ s.removeAttribute('value');
499
+ }
464
500
  else
465
- if( isString(v) )
466
- s.append( createText( d, v) );
467
- else
468
- s.append( obj2node(v,'value',s.ownerDocument) )
501
+ { const ve = isString(v) ? createText( d, v) : obj2node(v,'value',s.ownerDocument);
502
+ s.append( ve );
503
+ s.setAttribute('value',v);
504
+ }
469
505
  }
470
506
  return s
471
507
  })
@@ -513,11 +549,26 @@ const loadTemplateRoots = async ( src, dce )=>
513
549
  export function mergeAttr( from, to )
514
550
  { for( let a of from.attributes)
515
551
  try
516
- { a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
552
+ { const name = a.name;
553
+ if( name.startsWith('xmlns') )
554
+ continue;
555
+ if( a.namespaceURI )
556
+ { if( !to.hasAttributeNS(a.namespaceURI, name) || to.getAttributeNS(a.namespaceURI, name) !== a.value )
557
+ to.setAttributeNS( a.namespaceURI, name, a.value )
558
+ }else
559
+ { if( !to.hasAttribute(name) || to.getAttribute(name) !== a.value )
560
+ to.setAttribute( a.name, a.value )
561
+ }
517
562
  if( a.name === 'value')
518
563
  to.value = a.value
519
564
  }catch(e)
520
565
  { console.warn('attribute assignment error',e?.message || e); }
566
+ const ea = to.dceExportedAttributes
567
+ , aa = to.getAttribute('dce-exported-attributes')
568
+ , em = aa ? new Set( aa.split(' ') ) : null;
569
+ for( let a of to.getAttributeNames() )
570
+ if( !from.hasAttribute(a) && !ea?.has(a) && !em?.has(a) )
571
+ to.removeAttribute(a)
521
572
  }
522
573
  export function assureUnique(n, id=0)
523
574
  {
@@ -630,14 +681,19 @@ export const toXsl = (el, defParent) => {
630
681
  x.setAttribute( a.name, a.value );
631
682
  while(el.firstChild)
632
683
  x.append(el.firstChild);
684
+ const replacement = el.localName === 'if' || el.localName === 'choose' ? (() => {
685
+ const span = create('span');
686
+ span.append(x);
687
+ return span;
688
+ })() : x;
633
689
  if( el.parentElement )
634
- el.parentElement.replaceChild( x, el );
690
+ el.parentElement.replaceChild( replacement, el );
635
691
  else
636
692
  { const p = (el.parentElement || defParent)
637
693
  , arr = [...p.childNodes];
638
694
  arr.forEach((n, i) => {
639
695
  if (n === el)
640
- arr[i] = x;
696
+ arr[i] = replacement;
641
697
  });
642
698
  p.replaceChildren(...arr);
643
699
  }
@@ -650,7 +706,7 @@ CustomElement extends HTMLElement
650
706
  async connectedCallback()
651
707
  {
652
708
  if(this.firstElementChild && this.firstElementChild.localName !== 'template')
653
- console.warn('custom-element used without template wrapping content\n', this.outerHTML);
709
+ console.log('custom-element used without template wrapping content\n', this.outerHTML);
654
710
  const templateRoots = await loadTemplateRoots( attr( this, 'src' ), this )
655
711
  , tag = attr( this, 'tag' )
656
712
  , tagName = tag ? tag : 'dce-'+crypto.randomUUID();
@@ -681,11 +737,13 @@ CustomElement extends HTMLElement
681
737
  .map(splitSliceNames).flat();
682
738
 
683
739
  const { declaredAttributes, hardcodedAttributes, exposedAttributes } = templateDocs[0];
740
+ const dceExportedAttributes = new Set([...Object.keys(hardcodedAttributes), ...Object.keys(exposedAttributes)]);
684
741
 
685
742
  class DceElement extends HTMLElement
686
743
  {
687
744
  static get observedAttributes(){ return declaredAttributes; }
688
745
  #inTransform = 0;
746
+ get dceExportedAttributes(){ return dceExportedAttributes; }
689
747
  connectedCallback()
690
748
  { let payload = sanitizeBlankText(this.childNodes);
691
749
  if( this.firstElementChild?.tagName === 'TEMPLATE' )
@@ -719,8 +777,9 @@ CustomElement extends HTMLElement
719
777
  this.innerHTML='';
720
778
  const attrsRoot = injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) )
721
779
  , inAttrs = a=> this.hasAttribute(a) || [...attrsRoot.children].find(e=>e.localName === a);
780
+ mergeAttr( this, attrsRoot );
722
781
  Object.keys(hardcodedAttributes).map(a=> inAttrs(a) || attrsRoot.append(createXmlNode(a,hardcodedAttributes[a])) );
723
- declaredAttributes.map(a=> inAttrs(a) || attrsRoot.append(createXmlNode(a)) );
782
+ Object.keys(exposedAttributes).map(a=> inAttrs(a) || attrsRoot.append(createXmlNode(a)) );
724
783
 
725
784
  injectData( x, 'dataset', Object.keys( this.dataset ), k => createXmlNode( k, this.dataset[ k ] ) );
726
785
  const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) )
@@ -753,34 +812,42 @@ CustomElement extends HTMLElement
753
812
  const transform = this.transform = ()=>
754
813
  { if(this.#inTransform){ debugger }
755
814
  this.#inTransform = 1;
756
-
757
- const ff = xp.map( (p,i) =>
758
- { const f = p.transformToFragment(x.ownerDocument, document)
759
- if( !f )
760
- console.error( "XSLT transformation error. xsl:\n", xmlString(templateDocs[i]), '\nxml:\n', xmlString(x) );
761
- return f
762
- });
763
- ff.map( f =>
764
- { if( !f )
765
- return;
766
- assureUnique(f);
767
- merge( this, f.childNodes )
768
- })
769
-
770
- Object.entries(hardcodedAttributes).map(( [a,v] )=>
771
- { if( !this.hasAttribute(a) && v !== attr(this,a) )
772
- { this.setAttribute( a, v );
773
- this.#applyAttribute( a, v );
774
- }
775
- });
776
-
777
- Object.keys(exposedAttributes).map( a =>
778
- { let v = attr(this.firstElementChild,a);
779
- if( v !== attr(this,a) )
780
- { this.setAttribute( a, v );
781
- this.#applyAttribute( a, v );
782
- }
783
- });
815
+ const renderModel = ()=>
816
+ {
817
+ const ff = xp.map( (p,i) =>
818
+ { const f = p.transformToFragment(x.ownerDocument, document)
819
+ if( !f )
820
+ console.error( "XSLT transformation error. xsl:\n", xmlString(templateDocs[i]), '\nxml:\n', xmlString(x) );
821
+ return f
822
+ });
823
+ ff.map( f =>
824
+ { if( !f )
825
+ return;
826
+ assureUnique(f);
827
+ merge( this, f.childNodes )
828
+ })
829
+ let attrChangedCount = 0;
830
+ Object.entries(hardcodedAttributes).map(( [a,v] )=>
831
+ { if( !this.hasAttribute(a) && v !== attr(this,a) )
832
+ { this.setAttribute( a, v );
833
+ this.#applyAttribute( a, v );
834
+ attrChangedCount++;
835
+ }
836
+ });
837
+
838
+ Object.keys(exposedAttributes).map( a =>
839
+ { let v = attr(this.firstElementChild,a);
840
+ if( v !== attr(this,a) )
841
+ { this.setAttribute( a, v );
842
+ this.#applyAttribute( a, v );
843
+ attrChangedCount++;
844
+ }
845
+ });
846
+ return attrChangedCount;
847
+ };
848
+ if( renderModel() )
849
+ if( renderModel() )
850
+ console.warn("model update should not be the result of transform more than once");
784
851
 
785
852
  function getSliceTarget(el)
786
853
  { let r = el;
@@ -868,13 +935,15 @@ CustomElement extends HTMLElement
868
935
  #applyAttribute(name, newValue)
869
936
  { if( 'value' === name )
870
937
  this.value = newValue;
938
+ const attrs = this.xml.querySelector('attributes');
871
939
  let a = this.xml.querySelector(`attributes>${name}`);
872
940
  if( a )
873
941
  emptyNode(a).append( createText(a,newValue) );
874
942
  else
875
943
  { a = create( name, newValue, this.xml );
876
- this.xml.querySelector('attributes').append( a );
944
+ attrs.append( a );
877
945
  }
946
+ this.#inTransform || attrs.setAttribute(name,newValue);
878
947
 
879
948
  this.dispatchEvent(new CustomEvent('change', { bubbles: true,detail: { [name]: newValue }}))
880
949
  }
package/demo/a.html CHANGED
@@ -63,11 +63,18 @@
63
63
  <body>
64
64
 
65
65
 
66
+ <custom-element tag="dce-link2" >
67
+ <template>
66
68
 
67
- <custom-element src="./html-template.html#dwc-logo">
68
- <template><i>loading SVG from templates file ...</i></template>
69
- </custom-element>
69
+ <attribute name="p3" select="//attributes/p3 ?? 'def_P3' "></attribute>
70
70
 
71
+ p3: <code data-testid="t3">{$p3}</code>
72
+ </template>
73
+ </custom-element>
74
+ <section>
75
+ <dce-link2 id="dce2" p1="123" p2="override ignored as select is defined"></dce-link2>
76
+
77
+ </section>
71
78
 
72
79
  </body>
73
80
  </html>
@@ -22,14 +22,30 @@
22
22
  <nav>
23
23
  <a href="../index.html"><h3><code>custom-element</code> demo</h3></a>
24
24
  </nav>
25
- <html-demo-element legend="param as attributes definition" description="
26
- params needed to declare DCE attributes and track the attributes changes. It also be used by IDE and validation.
27
- ">
25
+ <main>
26
+ <p><code>attribute</code> is used for DCE attributes declaration and track the attributes changes. It also be used by IDE and validation.</p>
27
+ <p>The attribute can be changed by component itself and used as output to the container.
28
+ Usual case is when <code>value</code> attribute is updated from inside.</p>
29
+ <p>Initial value of attribute is available on the <code>attributes</code> node attribute as in <code>/datadom/attributes/@attr1</code></p>
30
+ <p>The current, i.e. including the changes by component itself, attribute value is a child node of <code>attributes</code> as in <code>/datadom/attributes/attr1</code>.</p>
31
+ <p>To define the attribute which is modified from within, the 3 parts are usually used as in <code>//s[//s/event] ?? //attributes/@v ?? 'def' </code>
32
+ <ol>
33
+ <li><code>//s[//s/event]</code> the slice <code>s</code> with event gives the slice value which was modified by user event like input</li>
34
+ <li><code>//attributes/@v</code> the attribute passed by container</li>
35
+ <li><code>'def' </code> the default value which used when no user input or attribute set by container</li>
36
+ </ol>
37
+ </p>
38
+ </main>
39
+ <html-demo-element legend="attributes definition" >
40
+ <p slot="description">
41
+ <code>attribute</code> is used for DCE attributes declaration and track the attributes changes. It also be used by IDE and validation.
42
+
43
+ </p>
28
44
  <template>
29
45
  <custom-element tag="dce-link" hidden>
30
46
  <attribute name="p1" >default_P1 </attribute>
31
47
  <attribute name="p2" select="'always_p2'" ></attribute>
32
- <attribute name="p3" select="//p3 ?? 'def_P3' " ></attribute>
48
+ <attribute name="p3" select="//attributes/@p3 ?? 'def_P3' " ></attribute>
33
49
  p1:{$p1} <br/> p2: {$p2} <br/> p3: {$p3}
34
50
  </custom-element>
35
51
  <dce-link id="dce1" ></dce-link>
@@ -44,13 +60,15 @@ params needed to declare DCE attributes and track the attributes changes. It als
44
60
  </template>
45
61
  </html-demo-element>
46
62
 
47
- <html-demo-element legend="attribute from slice" description="
48
- when slice value points to attribute, it would be populated on slice change
49
- ">
63
+ <html-demo-element legend="attribute from slice">
64
+ <p slot="description">
65
+ When slice value points to attribute, it would be populated on slice change.<br/>
50
66
  Type in the input field to see the variable $title change. <br/>
51
67
  Hover the mouse to see the title attribute text popup.<br/>
52
68
  Inspect DCE node in dev tools to see `title` attribute updated while typing.
53
69
 
70
+ </p>
71
+
54
72
  <template>
55
73
  <custom-element>
56
74
  <template>
@@ -62,6 +80,52 @@ when slice value points to attribute, it would be populated on slice change
62
80
  </template>
63
81
  </html-demo-element>
64
82
 
83
+
84
+ <html-demo-element legend="V attribute matches input value" description="
85
+ Type in the input field and observe in DevTools the V attribute changed.
86
+ ">
87
+ <template>
88
+ <custom-element tag="x-input" >
89
+ <template>
90
+ <attribute name="is-changed" select="count(//s/event) &gt; 0"></attribute>
91
+ <attribute name="v" select="//s[//s/event] ?? //attributes/@v ?? 'def' "></attribute>
92
+ /datadom/attributes/v='{/datadom/attributes/v}'<br/>
93
+ same as v='{v}'<br/>
94
+ same as $v='{$v}'<br/>
95
+ //attributes/@v='{//attributes/@v}'<br/>
96
+ //s='{//s}'<br/>
97
+ is-changed ={ is-changed }<br/>
98
+ <input slice="s" slice-event="input" value="{//attributes/v}"/>
99
+ </template>
100
+ </custom-element>
101
+ <x-input></x-input>
102
+ <x-input v="V1"></x-input>
103
+ </template>
104
+ </html-demo-element>
105
+
106
+ <html-demo-element legend="attribute defaults, from container, and from slice" description="
107
+ Type in the input field and observe in DevTools the V attribute changed.
108
+ ">
109
+ <template>
110
+ <custom-element tag="attr-demo">
111
+ <template>
112
+ <variable name="has-input" select="count(//s/*) &gt; 0"></variable>
113
+ <attribute name="v" select="//s[//s/event] ?? //attributes/@v ?? 'def' "></attribute>
114
+ //attributes/v='{//attributes/v}'<br/>
115
+ //attributes/@v='{//attributes/@v}'<br/>
116
+ $v='{$v}'<br/>
117
+ //s='{//s}'<br/>
118
+ A='{//s[//s/event] | //attributes/v[not(//s/event)]}'<br/>
119
+ has-input ={ $has-input }<br/>
120
+ <input slice="s" slice-event="input" />
121
+ </template>
122
+ </custom-element>
123
+
124
+ <attr-demo></attr-demo>
125
+ <attr-demo v="From Container"></attr-demo>
126
+ </template>
127
+ </html-demo-element>
128
+
65
129
  <details>
66
130
  <summary>Attributes processing</summary>
67
131
  To be available in template, <code>custom-element</code> attributes should be
@@ -1,6 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3
3
  <head>
4
+ <meta charset="utf-8">
4
5
  <title>template based on HTML file</title>
5
6
  <style>svg {
6
7
  width: 4rem;
@@ -8,9 +9,9 @@
8
9
  </head>
9
10
  <body>
10
11
  <script>console.error('Stranger danger!')</script>
11
- <b id="wave">👋</b>
12
- <b id="ok">👌</b>
13
- <svg id="dwc-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216 209.18">
12
+ <b id="wave" data-testid="wave" >👋</b>
13
+ <b id="ok" data-testid="ok" >👌</b>
14
+ <svg id="dwc-logo" data-testid="dwc-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216 209.18">
14
15
  <defs>
15
16
  <style>.cls-1 {
16
17
  fill: #c2e6f1;
@@ -73,7 +74,7 @@
73
74
  <path class="cls-1"
74
75
  d="m184.02,96.93l20.64-11.92c.47-.27.47-.79,0-1.06l-20.65-11.92h0c-4.44-2.57-8.22-2.57-12.67,0l-20.65,11.92c-.47.27-.47.79,0,1.06l20.64,11.92c4.44,2.57,8.22,2.57,12.67,0h0Z"/>
75
76
  </svg>
76
- <math id="sophomores-dream" display="block">
77
+ <math id="sophomores-dream" data-testid="ml-test" display="block">
77
78
  <mrow>
78
79
  <msubsup>
79
80
  <mo>∫</mo>