@epa-wg/custom-element 0.0.18 → 0.0.20

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
@@ -22,16 +22,17 @@ UI is re-rendered on each data slice change triggered by initialization or DOM e
22
22
  <details>
23
23
  <summary> What is DCE? </summary>
24
24
  DCE provides the next level of abstraction in HTML - native composition. With native implementation which is
25
- streaming parser, streaming transformation, multithreading. native assumes the C/Rust compiled code. There is no place for JavaScript except of polyfill and ability to extend DCE, which otherwise has to be native.
25
+ streaming parser, streaming transformation, multithreading. native assumes the C/Rust compiled code.
26
+ There is no place for JavaScript except of polyfill and ability to extend DCE, which otherwise has to be native.
26
27
 
27
28
  The composition assumes the fully functional template and ability to call the template with parameters( custom tag + attributes) .
28
29
 
29
- As the next to HTML abstraction layer - composition, it needs and provide:
30
+ As the next to HTML abstraction layer - **composition**, it provides:
30
31
  * ability to use dependencies as from withing the page as from external file/lib via src attribute and # in URL
31
32
  * ability to treat external content via content-type like html, SVG, images, video with own template rendering
32
33
  * provide styles and embedded DCE declarations in own and named(lib) scope, sharing the scoped registry.
33
34
 
34
- As the next to composition layer of **functional component** it provides
35
+ After composition the layer of **functional component** provides
35
36
  * data layer with access to attributes/payload(+slots), dataset, data bound slice
36
37
  * means in template to use the data selector for condition/enumeration/text injection into attributes and DOM
37
38
  * Set of native primitives to support browser APIs declaratively: location,storage, http request which bonded to slice and as result to reactive UI.
@@ -58,12 +59,35 @@ npm i -P @epa-wg/custom-element
58
59
  yarn add @epa-wg/custom-element
59
60
  ```
60
61
 
61
- ## [enable IDE support](ide/IDE.md)
62
+ ## Enable IDE support
63
+ [IDE.md](ide/IDE.md)
62
64
 
63
65
 
64
66
 
65
67
  ## [Live demo 🔗][demo-url]
68
+
69
+ ### Interactivity via data `slice` triggered by events
70
+ ```html
71
+ <custom-element>
72
+ <input slice="typed"> //slice/typed : {//slice/typed}
73
+ </custom-element>
74
+
75
+ <custom-element>
76
+ <template>
77
+ <button slice="clickcount"
78
+ slice-event="click"
79
+ slice-value="//clickcount + 1" > + </button>
80
+ <input slice="clickcount" type="number" value="{//clickcount ?? 0}">
81
+ Click count: { //clickcount }
82
+ </template>
83
+ </custom-element>
84
+ ```
85
+ More on `slice` concept in [slice and events demo page][slice-demo-url]
86
+
87
+ ### Templating power
88
+ comes from XSLT and XPath. Which is natively implemented in all current browsers, globally tested and well documented.
66
89
  ```html
90
+
67
91
  <custom-element tag="pokemon-tile" hidden>
68
92
  <h3>{title}</h3> <!-- title is an attribute in instance
69
93
  mapped into /*/attributes/title -->
@@ -315,6 +339,7 @@ within template
315
339
  [git-test-url]: https://github.com/EPA-WG/custom-element-test
316
340
  [demo-url]: https://unpkg.com/@epa-wg/custom-element@0.0/index.html
317
341
  [css-demo-url]: https://unpkg.com/@epa-wg/custom-element@0.0/demo/scoped-css.html
342
+ [slice-demo-url]: https://unpkg.com/@epa-wg/custom-element@0.0/demo/data-slices.html
318
343
  [hex-grid-url]: https://unpkg.com/@epa-wg/custom-element@0.0/demo/hex-grid.html
319
344
  [hex-grid-image]: demo/hex-grid-transform.png
320
345
  [local-storage-demo]: https://unpkg.com/@epa-wg/custom-element@0.0/demo/local-storage.html
@@ -323,9 +348,9 @@ within template
323
348
  [github-image]: https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/mark-github.svg
324
349
  [npm-image]: https://img.shields.io/npm/v/@epa-wg/custom-element.svg
325
350
  [npm-url]: https://npmjs.org/package/@epa-wg/custom-element
326
- [coverage-image]: https://unpkg.com/@epa-wg/custom-element-test@0.0.18/coverage/coverage.svg
327
- [coverage-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.18/coverage/lcov-report/index.html
328
- [storybook-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.18/storybook-static/index.html?path=/story/welcome--introduction
351
+ [coverage-image]: https://unpkg.com/@epa-wg/custom-element-test@0.0.20/coverage/coverage.svg
352
+ [coverage-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.20/coverage/lcov-report/index.html
353
+ [storybook-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.20/storybook-static/index.html?path=/story/welcome--introduction
329
354
  [sandbox-url]: https://stackblitz.com/github/EPA-WG/custom-element?file=index.html
330
355
  [webcomponents-url]: https://www.webcomponents.org/element/@epa-wg/custom-element
331
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.18",
119
+ "version": "0.0.20",
120
120
  "js-types-syntax": "typescript",
121
121
  "description-markup": "markdown",
122
122
  "contributions": {
package/custom-element.js CHANGED
@@ -7,9 +7,12 @@ const XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform'
7
7
 
8
8
  const attr = (el, attr)=> el.getAttribute?.(attr)
9
9
  , isText = e => e.nodeType === 3
10
- , create = ( tag, t = '', d=document ) => ( e => ((e.innerText = t||''),e) )((d.ownerDocument || d ).createElement( tag ))
10
+ , isString = s => typeof s === 'string'
11
+ , isNode = e => e && typeof e.nodeType === 'number'
12
+ , create = ( tag, t = '', d=document ) => ( e => ((t && e.append(createText(d.ownerDocument||d, t))),e) )((d.ownerDocument || d ).createElement( tag ))
11
13
  , createText = ( d, t) => (d.ownerDocument || d ).createTextNode( t )
12
- , emptyNode = n=> { while(n.firstChild) n.firstChild.remove(); return n; }
14
+ , removeChildren = n => { while(n.firstChild) n.firstChild.remove(); return n; }
15
+ , emptyNode = n => { n.getAttributeNames().map( a => n.removeAttribute(a) ); return removeChildren(n); }
13
16
  , createNS = ( ns, tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElementNS( ns, tag ))
14
17
  , xslNs = x => ( x?.setAttribute('xmlns:xsl', XSL_NS_URL ), x )
15
18
  , xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(x) )
@@ -90,6 +93,32 @@ Json2Xml( o, tag )
90
93
  ret.push("/>");
91
94
  return ret.join('\n');
92
95
  }
96
+
97
+ export function
98
+ obj2node( o, tag, doc )
99
+ { const t = typeof o;
100
+ if( t === 'function'){debugger}
101
+ if( t === 'string' )
102
+ return create(tag,o,doc);
103
+ if( t === 'number' )
104
+ return create(tag,''+o,doc);
105
+
106
+ if( o instanceof Array )
107
+ { const ret = create('array');
108
+ o.map( ae => ret.append( obj2node(ae,tag,doc)) );
109
+ return ret
110
+ }
111
+ const ret = create(tag,'',doc);
112
+ for( let k in o )
113
+ if( isNode(o[k]) || typeof o[k] ==='function' || o[k] instanceof Window )
114
+ continue
115
+ else
116
+ if( typeof o[k] !== "object" )
117
+ ret.setAttribute(k, o[k] );
118
+ else
119
+ ret.append(obj2node(o[k], k, doc))
120
+ return ret;
121
+ }
93
122
  export function
94
123
  tagUid( node )
95
124
  { // {} to xsl:value-of
@@ -131,7 +160,7 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
131
160
  {
132
161
  if( templateNode.tagName === S || templateNode.documentElement?.tagName === S )
133
162
  return tagUid(templateNode)
134
- const sanitizeXsl = xml2dom(`<xsl:stylesheet version="1.0" xmlns:xsl="${ XSL_NS_URL }" xmlns:xhtml="${ HTML_NS_URL }" xmlns:exsl="${EXSL_NS_URL}" exclude-result-prefixes="exsl" >
163
+ const sanitizeXsl = xml2dom(`<xsl:stylesheet version="1.0" xmlns:xsl="${ XSL_NS_URL }" xmlns:xhtml="${ HTML_NS_URL }" xmlns:exsl="${EXSL_NS_URL}" exclude-result-prefixes="exsl" >
135
164
  <xsl:output method="xml" />
136
165
  <xsl:template match="/"><dce-root xmlns="${ HTML_NS_URL }"><xsl:apply-templates select="*"/></dce-root></xsl:template>
137
166
  <xsl:template match="*[name()='template']"><xsl:apply-templates mode="sanitize" select="*|text()"/></xsl:template>
@@ -212,25 +241,38 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
212
241
  if( !fr )
213
242
  return console.error("transformation error",{ xml:tc.outerHTML, xsl: xmlString( sanitizeXsl ) });
214
243
  const params = [];
215
- [...fr.querySelectorAll('dce-root>attribute')].forEach(p=>
216
- { p = cloneAs(p,'xsl:param');
244
+ [...fr.querySelectorAll('dce-root>attribute')].forEach( a=>
245
+ {
246
+ const p = cloneAs(a,'xsl:param')
247
+ , name = attr(a,'name');
217
248
  payload.append(p);
218
249
  let select = attr(p,'select')?.split('??')
219
250
  if( !select)
220
- { select = ['//'+attr(p, 'name'), `'${p.textContent}'`];
251
+ { select = ['//'+name, `'${p.textContent}'`];
221
252
  emptyNode(p);
253
+ p.setAttribute('name',name);
222
254
  }
255
+ let val;
223
256
  if( select?.length>1 ){
224
257
  p.removeAttribute('select');
225
258
  const c = $( xslDom, 'template[match="ignore"]>choose').cloneNode(true);
226
- c.firstElementChild.setAttribute('test',select[0]);
227
259
  emptyNode(c.firstElementChild).append( createText(c,'{'+select[0]+'}'));
228
260
  emptyNode(c.lastElementChild ).append( createText(c,'{'+select[1]+'}'));
229
- p.append(c)
230
- }
261
+ c.firstElementChild.setAttribute('test',select[0]);
262
+ p.append(c);
263
+ val = c.cloneNode(true);
264
+ }else
265
+ val=cloneAs(a,'xsl:value-of');
266
+ val.removeAttribute('name');
267
+ a.append(val);
268
+ a.removeAttribute('select');
231
269
  params.push(p)
232
270
  });
233
-
271
+ [...fr.querySelectorAll('[value]')].filter(el=>el.getAttribute('value').match( /\{(.*)\?\?(.*)\}/g )).forEach(el=>
272
+ { const v = attr(el,'value');
273
+ if(v)
274
+ el.setAttribute('value', evalCurly(v));
275
+ });
234
276
  for( const c of fr.childNodes )
235
277
  payload.append(xslDom.importNode(c,true))
236
278
 
@@ -287,18 +329,63 @@ deepEqual(a, b, O=false)
287
329
  return O
288
330
  return true;
289
331
  }
290
-
332
+ export const
333
+ assureSlices = ( root, names) =>
334
+ names.split('|').map(n=>n.trim()).map( xp =>
335
+ { if(xp.includes('/'))
336
+ { const ret = [], r = root.ownerDocument.evaluate( xp, root );
337
+ for( let n; n = r.iterateNext(); )
338
+ ret.push( n )
339
+ return ret
340
+ }
341
+ return [...root.childNodes].find(n=>n.localName === xp) || create(xp);
342
+ }).flat();
343
+
344
+ /**
345
+ *
346
+ * @param x slice node
347
+ * @param sliceNames slice name, xPath in /datadom/slice/
348
+ * @param ev Event obj
349
+ * @param dce
350
+ */
291
351
  export function
292
- injectSlice( x, s, data )
352
+ event2slice( x, sliceNames, ev, dce )
293
353
  {
294
- const isString = typeof data === 'string' ;
295
- const createXmlNode = ( tag, t = '' ) => ( e => ((e.append( createText(x, t||''))),e) )(x.ownerDocument.createElement( tag ))
296
- const el = isString
297
- ? createXmlNode(s, data)
298
- : document.adoptNode( xml2dom( Json2Xml( data, s ) ).documentElement);
299
- [...x.children].filter( e=>e.localName === s ).map( el=>el.remove() );
300
- el.data = data
301
- x.append(el);
354
+ // evaluate slices[]
355
+ // inject @attributes
356
+ // inject event
357
+ // evaluate slice-value
358
+ // slice[i] = slice-value
359
+ assureSlices(x,sliceNames).map( s =>
360
+ {
361
+ const d = x.ownerDocument
362
+ , el = ev.sliceEventSource
363
+ , sel = ev.sliceElement
364
+ , cleanSliceValue = ()=>[...s.childNodes].filter(n=>n.nodeType===3 || n.localName==='value').map(n=>n.remove());
365
+ el.getAttributeNames().map( a => s.setAttribute( a, attr(el,a) ) );
366
+ [...s.childNodes].filter(n=>n.localName==='event').map(n=>n.remove());
367
+ ev.type==='init' && cleanSliceValue();
368
+ s.append( obj2node( ev, 'event', d ) );
369
+ if( sel.hasAttribute('slice-value') )
370
+ { if( el.value === undefined)
371
+ s.removeAttribute('value')
372
+ else
373
+ s.setAttribute('value', el.value );
374
+ const v = xPath( attr( sel, 'slice-value'),s );
375
+ cleanSliceValue();
376
+ s.append( createText( d, v ) );
377
+ }else
378
+ { const v = el.value ?? attr( sel, 'value' ) ;
379
+ cleanSliceValue();
380
+ if( v === null || v === undefined )
381
+ [...s.childNodes].filter(n=>n.localName!=='event').map(n=>n.remove());
382
+ else
383
+ if( isString(v) )
384
+ s.append( createText( d, v) );
385
+ else
386
+ s.append( obj2node(v,'value',s.ownerDocument) )
387
+ }
388
+ })
302
389
  }
303
390
 
304
391
  function forEach$( el, css, cb){
@@ -339,7 +426,10 @@ export function mergeAttr( from, to )
339
426
  return
340
427
  }
341
428
  for( let a of from.attributes)
342
- a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
429
+ { a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
430
+ if( a.name === 'value')
431
+ to.value = a.value
432
+ }
343
433
  }
344
434
  export function assureUnique(n, id=0)
345
435
  {
@@ -364,6 +454,8 @@ export function assureUnique(n, id=0)
364
454
  }
365
455
  export function merge( parent, fromArr )
366
456
  {
457
+ if(!fromArr.length)
458
+ return removeChildren(parent);
367
459
  const id2old = {};
368
460
  for( let c of parent.childNodes)
369
461
  { ASSERT( !id2old[c.dceId] );
@@ -374,26 +466,57 @@ export function merge( parent, fromArr )
374
466
  id2old[attr(c, 'data-dce-id') || 0] = c;
375
467
  }
376
468
  for( let e of [...fromArr] )
377
- {
378
- const o = id2old[ attr(e, 'data-dce-id') || e.dceId ];
469
+ { const k = attr(e, 'data-dce-id') || e.dceId;
470
+ const o = id2old[ k ];
379
471
  if( o )
380
472
  { if( isText(e) )
381
473
  { if( o.nodeValue !== e.nodeValue )
382
474
  o.nodeValue = e.nodeValue;
383
475
  }else
384
- { mergeAttr(o,e)
476
+ { mergeAttr(e,o)
385
477
  if( o.childNodes.length || e.childNodes.length )
386
478
  merge(o, e.childNodes)
387
479
  }
480
+ delete id2old[ k ]
388
481
  }else
389
482
  parent.append( e )
390
483
  }
484
+ for( let v of Object.values(id2old) )
485
+ v.remove();
391
486
  }
392
487
  export function assureUID(n,attr)
393
488
  { if( !n.hasAttribute(attr) )
394
489
  n.setAttribute(attr, crypto.randomUUID());
395
490
  return n.getAttribute(attr)
396
491
  }
492
+ export const evalCurly = s =>
493
+ { const exp = [...s?.matchAll( /([^{}]*)(\{)([^}]+)}([^{}]*)/g ) ].map(l=>`${l[1]}{${ xPathDefaults(l[3] )}}${l[4]}`);
494
+ return exp.join('');
495
+ }
496
+ export const xPathDefaults = x=>
497
+ { if(!x.trim())
498
+ return x;
499
+ const xx = x.split('??')
500
+ , a = xx.shift()
501
+ , b = xPathDefaults(xx.join('??'));
502
+
503
+ return xx.length ? `concat( ${a} , substring( ${b} , (1+string-length( ${b} )) * string-length( ${a} ) ) )`: x
504
+ // return xx.length ? `${a}|(${xPathDefaults(xx.join('??'))})[not(${a})]`: a
505
+ }
506
+ export const xPath = (x,root)=>
507
+ { x = xPathDefaults(x);
508
+
509
+ const it = root.ownerDocument.evaluate(x, root);
510
+ switch( it.resultType )
511
+ { case XPathResult.NUMBER_TYPE: return it.numberValue;
512
+ case XPathResult.STRING_TYPE: return it.stringValue;
513
+ }
514
+
515
+ let ret = '';
516
+ for( let n ;n=it.iterateNext(); )
517
+ ret += n.textContent;
518
+ return ret
519
+ }
397
520
  export const xslTags = 'stylesheet,transform,import,include,strip-space,preserve-space,output,key,decimal-format,namespace-alias,template,value-of,copy-of,number,apply-templates,apply-imports,for-each,sort,if,choose,when,otherwise,attribute-set,call-template,with-param,variable,param,text,processing-instruction,element,attribute,comment,copy,message,fallback'.split(',');
398
521
  export const toXsl = (el, defParent) => {
399
522
  const x = create('xsl:'+el.localName);
@@ -436,19 +559,23 @@ CustomElement extends HTMLElement
436
559
 
437
560
  Object.defineProperty( this, "xsltString", { get: ()=>templateDocs.map( td => xmlString(td) ).join('\n') });
438
561
 
439
- const dce = this;
440
- const sliceNames = [...this.templateNode.querySelectorAll('[slice]')].map(e=>attr(e,'slice'));
562
+ const dce = this
563
+ , sliceNodes = [...this.templateNode.querySelectorAll('[slice]')]
564
+ , sliceNames = sliceNodes.map(e=>attr(e,'slice')).filter(n=>!n.includes('/')).filter((v, i, a)=>a.indexOf(v) === i)
565
+ , declaredAttributes = templateDocs.reduce( (ret,t) => { if( t.params ) ret.push( ...t.params ); return ret; }, [] );
566
+
441
567
  class DceElement extends HTMLElement
442
568
  {
443
- static get observedAttributes()
444
- { return templateDocs.reduce( (ret,t) =>
445
- { if( t.params ) ret.push( ...t.params.map(e=>attr(e,'name')) );
446
- return ret;
447
- }, [] );
448
- }
569
+ static get observedAttributes(){ return declaredAttributes.map( a=>attr(a,'name')); }
570
+ #inTransform = 0;
449
571
  connectedCallback()
450
- { if( this.firstElementChild?.tagName === 'TEMPLATE' )
451
- { const t = this.firstElementChild;
572
+ { let payload = this.childNodes;
573
+ if( this.firstElementChild?.tagName === 'TEMPLATE' )
574
+ {
575
+ const t = this.firstElementChild;
576
+ t.remove();
577
+ payload = t.content.childNodes;
578
+
452
579
  for( const n of [...t.content.childNodes] )
453
580
  if( n.localName === 'style' ){
454
581
  const id = assureUID(this,'data-dce-style')
@@ -459,9 +586,6 @@ CustomElement extends HTMLElement
459
586
  t.insertAdjacentElement('beforebegin',n);
460
587
  else if(n.nodeType===3)
461
588
  t.insertAdjacentText('beforebegin',n.data);
462
-
463
- t.remove();
464
-
465
589
  }
466
590
  const x = xml2dom( '<datadom/>' ).documentElement;
467
591
  const createXmlNode = ( tag, t = '' ) => ( e =>
@@ -469,11 +593,12 @@ CustomElement extends HTMLElement
469
593
  e.append( createText( x, t ))
470
594
  return e;
471
595
  })(x.ownerDocument.createElement( tag ))
472
- injectData( x, 'payload' , this.childNodes, assureSlot );
596
+ injectData( x, 'payload' , payload , assureSlot );
473
597
  this.innerHTML='';
474
598
  injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
475
599
  injectData( x, 'dataset', Object.keys( this.dataset ), k => createXmlNode( k, this.dataset[ k ] ) );
476
- const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) );
600
+ const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) )
601
+ , sliceXPath = x => xPath(x, sliceRoot);
477
602
  this.xml = x;
478
603
 
479
604
  const sliceEvents=[];
@@ -481,10 +606,10 @@ CustomElement extends HTMLElement
481
606
  { const processed = {}
482
607
 
483
608
  for(let ev; ev = sliceEvents.pop(); )
484
- { const s = attr( ev.target, 'slice');
609
+ { const s = attr( ev.sliceElement, 'slice');
485
610
  if( processed[s] )
486
611
  continue;
487
- injectSlice( sliceRoot, s, 'object' === typeof ev.detail ? {...ev.detail}: ev.detail );
612
+ event2slice( sliceRoot, s, ev, this );
488
613
  processed[s] = ev;
489
614
  }
490
615
  Object.keys(processed).length !== 0 && transform();
@@ -493,10 +618,7 @@ CustomElement extends HTMLElement
493
618
 
494
619
  this.onSlice = ev=>
495
620
  { ev.stopPropagation?.();
496
- const s = attr( ev.target, 'slice')
497
- if( deepEqual( ev.detail, [...sliceRoot.children].find( e=>e.localName === s )?.data ) )
498
- return
499
-
621
+ ev.sliceEventSource = ev.currentTarget || ev.target;
500
622
  sliceEvents.push(ev);
501
623
  if( !timeoutID )
502
624
  timeoutID = setTimeout(()=>
@@ -505,7 +627,9 @@ CustomElement extends HTMLElement
505
627
  },10);
506
628
  };
507
629
  const transform = this.transform = ()=>
508
- {
630
+ { if(this.#inTransform){ debugger }
631
+ this.#inTransform = 1;
632
+
509
633
  const ff = xp.map( (p,i) =>
510
634
  { const f = p.transformToFragment(x.ownerDocument, document)
511
635
  if( !f )
@@ -518,35 +642,55 @@ CustomElement extends HTMLElement
518
642
  assureUnique(f);
519
643
  merge( this, f.childNodes )
520
644
  })
521
- const changeCb = el=>this.onSlice({ detail: el[attr(el,'slice-prop') || 'value'], target: el })
522
- , hasInitValue = el => el.hasAttribute('slice-prop') || el.hasAttribute('value') || el.value;
645
+
646
+ DceElement.observedAttributes.map( a =>
647
+ { let v = attr(this.firstElementChild,a);
648
+ if( v !== attr(this,a) )
649
+ { this.setAttribute( a, v );
650
+ this.#applyAttribute( a, v );
651
+ }
652
+ })
523
653
 
524
654
  forEach$( this,'[slice]', el =>
525
655
  { if( !el.dceInitialized )
526
656
  { el.dceInitialized = 1;
527
- el.addEventListener( attr(el,'slice-update')|| 'change', ()=>changeCb(el) )
528
- if( hasInitValue(el) )
529
- changeCb(el)
657
+ const evs = attr(el,'slice-event');
658
+ (evs || 'change')
659
+ .split(' ')
660
+ .forEach( t=> (el.localName==='slice'? el.parentElement : el)
661
+ .addEventListener( t, ev=>
662
+ { ev.sliceElement = el;
663
+ this.onSlice(ev)
664
+ } ));
665
+ if( !evs || evs.includes('init') )
666
+ { if( el.hasAttribute('slice-value') || el.hasAttribute('value') || el.value )
667
+ this.onSlice({type:'init', target: el, sliceElement:el })
668
+ else
669
+ el.value = sliceXPath( attr(el,'slice') )
670
+ }
530
671
  }
531
- })
672
+ });
673
+ this.#inTransform = 0;
532
674
  };
533
675
  transform();
534
676
  applySlices();
535
677
  }
536
- attributeChangedCallback(name, oldValue, newValue)
537
- { if( !this.xml )
538
- return;
539
- let a = this.xml.querySelector(`attributes>${name}`);
678
+ #applyAttribute(name, newValue)
679
+ { let a = this.xml.querySelector(`attributes>${name}`);
540
680
  if( a )
541
- emptyNode(a).append( createText(a,newValue));
681
+ emptyNode(a).append( createText(a,newValue) );
542
682
  else
543
683
  { a = create( name, newValue, this.xml );
544
- a.append( createText(a,newValue) );
545
684
  this.xml.querySelector('attributes').append( a );
546
685
  }
547
-
686
+ }
687
+ attributeChangedCallback(name, oldValue, newValue)
688
+ { if( !this.xml || this.#inTransform )
689
+ return;
690
+ this.#applyAttribute(name, newValue);
548
691
  this.transform(); // needs throttling
549
692
  }
693
+
550
694
  get dce(){ return dce }
551
695
  }
552
696
  if(tag)
package/demo/a.html CHANGED
@@ -4,30 +4,70 @@
4
4
  <head>
5
5
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
6
  <title>custom-element Declarative Custom Element implementation demo</title>
7
- <link rel="icon" href="./wc-square.svg" />
8
- <script type="module" src="../location-element.js"></script>
7
+ <link rel="icon" href="./wc-square.svg"/>
8
+ <script type="module" src="../http-request.js"></script>
9
+ <script type="module" src="../local-storage.js"></script>
9
10
  <script type="module" src="../custom-element.js"></script>
10
11
 
11
12
  <style>
12
13
  @import "./demo.css";
13
14
 
14
- button{ background: forestgreen; }
15
- table{ min-width: 16rem; }
16
- td{ border-bottom: 1px solid silver; }
17
- tfoot td{ border-bottom: none; }
18
- td,th{text-align: right; }
19
- caption{ padding: 1rem; font-weight: bolder; font-family: sans-serif; }
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;
29
+ }
30
+
31
+ td, th {
32
+ text-align: right;
33
+ }
34
+
35
+ caption {
36
+ padding: 1rem;
37
+ font-weight: bolder;
38
+ font-family: sans-serif;
39
+ }
20
40
  </style>
21
41
  </head>
22
42
  <body>
23
43
 
24
- <custom-element tag="dce-link" hidden>
25
- <param name="p1" >default_P1 </param>
26
- <param name="p2" select="'always_p2'" ></param>
27
- <param name="p3" select="//p3 ?? 'def_P3' " ></param>
28
- p1:{$p1} <br/> p2: {$p2} <br/> p3: {$p3}
29
- </custom-element>
30
- <dce-link id="dce1" ></dce-link>
44
+ <custom-element>
45
+ <template>
46
+ <button slice="url-string" slice-value="'https://pokeapi.co/api/v2/pokemon?limit=6'" slice-event="click"
47
+ >⬇️https://pokeapi.co/api/v2/pokemon?limit=6</button>
48
+ <input slice="url-string" value="{ //url-string ?? '' }" style="width:100%"/>
49
+ <button slice="fetch-url" slice-event="click" slice-value="//url-string"> GET </button>
50
+ <http-request
51
+ url="{//fetch-url}"
52
+ slice="request_slice"
53
+ type="text"
54
+ mode="cors"
55
+ ></http-request>
56
+ //fetch-url : <code>{//fetch-url}</code>
57
+ <!-- <for-each select="//slice/request_slice/value/*">-->
58
+ <!-- <ul>-->
59
+ <!-- <var data-testid="request-section"><value-of select='name(.)'/></var>-->
60
+ <!-- <for-each select="@*">-->
61
+ <!-- <div>-->
62
+ <!-- <var data-testid="section-attribute">@<value-of select='local-name(.)'/></var>-->
63
+ <!-- =-->
64
+ <!-- <code><value-of select='.'/></code>-->
65
+ <!-- </div>-->
66
+ <!-- </for-each>-->
67
+ <!-- </ul>-->
68
+ <!-- </for-each>-->
69
+ </template>
70
+ </custom-element>
31
71
 
32
72
  </body>
33
73
  </html>