Skip to content

Commit

Permalink
Merge pull request #56 from EPA-WG/develop
Browse files Browse the repository at this point in the history
0.0.22
  • Loading branch information
sashafirsov authored Jun 25, 2024
2 parents 22fb11c + fd63f35 commit 7e1249c
Show file tree
Hide file tree
Showing 12 changed files with 361 additions and 52 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,9 @@ within template
[github-image]: https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/mark-github.svg
[npm-image]: https://img.shields.io/npm/v/@epa-wg/custom-element.svg
[npm-url]: https://npmjs.org/package/@epa-wg/custom-element
[coverage-image]: https://unpkg.com/@epa-wg/[email protected].21/coverage/src/custom-element/coverage.svg
[coverage-url]: https://unpkg.com/@epa-wg/[email protected].21/coverage/src/custom-element/index.html
[storybook-url]: https://unpkg.com/@epa-wg/[email protected].21/storybook-static/index.html?path=/story/welcome--introduction
[coverage-image]: https://unpkg.com/@epa-wg/[email protected].22/coverage/src/custom-element/coverage.svg
[coverage-url]: https://unpkg.com/@epa-wg/[email protected].22/coverage/src/custom-element/index.html
[storybook-url]: https://unpkg.com/@epa-wg/[email protected].22/storybook-static/index.html?path=/story/welcome--introduction
[sandbox-url]: https://stackblitz.com/github/EPA-WG/custom-element?file=index.html
[webcomponents-url]: https://www.webcomponents.org/element/@epa-wg/custom-element
[webcomponents-img]: https://img.shields.io/badge/webcomponents.org-published-blue.svg
Expand Down
2 changes: 1 addition & 1 deletion bin/xslDtd2Ide.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ writeFileSync( '.././ide/customData-xsl.json', JSON.stringify( vsCode, undefined
const intelliJ = {
"$schema": "http://json.schemastore.org/web-types",
"name": "@epa-wg/custom-element",
"version": "0.0.21",
"version": "0.0.22",
"js-types-syntax": "typescript",
"description-markup": "markdown",
"contributions": {
Expand Down
4 changes: 4 additions & 0 deletions custom-element.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export function log(x: any): void;
export function deepEqual(a: any, b:any): boolean|0;
export function xml2dom(xmlString:string): Document;
export function xmlString(doc:Node|Document): string;
export function obj2node(o:any, tag:string, doc:Document): HTMLElement;
export function tagUid(node:HTMLElement): HTMLElement;

/**
* @summary Declarative Custom Element as W3C proposal PoC with native(XSLT) based templating
Expand Down
145 changes: 103 additions & 42 deletions custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const attr = (el, attr)=> el.getAttribute?.(attr)
, createText = ( d, t) => (d.ownerDocument || d ).createTextNode( t )
, removeChildren = n => { while(n.firstChild) n.firstChild.remove(); return n; }
, emptyNode = n => { n.getAttributeNames().map( a => n.removeAttribute(a) ); return removeChildren(n); }
, createNS = ( ns, tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElementNS( ns, tag ))
, xslNs = x => ( x?.setAttribute('xmlns:xsl', XSL_NS_URL ), x )
, xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(x) )
, cloneAs = (p,tag) =>
Expand All @@ -23,7 +22,7 @@ const attr = (el, attr)=> el.getAttribute?.(attr)
while( p.firstChild )
px.append(p.firstChild);
return px;
}
};

function
ASSERT(x)
Expand Down Expand Up @@ -66,17 +65,22 @@ assureSlot( e )
export function
obj2node( o, tag, doc )
{ const t = typeof o;
if( t === 'function'){debugger}
if( t === 'string' )
return create(tag,o,doc);
if( t === 'number' )
return create(tag,''+o,doc);

if( o instanceof Array )
{ const ret = create('array');
{ const ret = create('array','',doc);
o.map( ae => ret.append( obj2node(ae,tag,doc)) );
return ret
}
if( o instanceof FormData )
{ const ret = create('form-data','',doc);
for( const p of o )
ret.append( obj2node(p[1],p[0],doc) );
return ret
}
const ret = create(tag,'',doc);
for( let k in o )
if( isNode(o[k]) || typeof o[k] ==='function' || o[k] instanceof Window )
Expand All @@ -91,13 +95,14 @@ obj2node( o, tag, doc )
export function
tagUid( node )
{ // {} to xsl:value-of
forEach$(node,'*',d => [...d.childNodes].filter( e=>e.nodeType === 3 ).forEach( e=>
{ if( e.parentNode.localName === 'style' )
return;
const m = e.data.matchAll( /{([^}]*)}/g );
forEach$(node,'*',d => [...d.childNodes]
.filter( e => e.nodeType === 3 && e.parentNode.localName !== 'style' && e.data )
.forEach( e=>
{ const s = e.data,
m = s.matchAll( /{([^}]*)}/g );
if(m)
{ let l = 0
, txt = t => createText(e,t||'')
, txt = t => createText(e,t)
, tt = [];
[...m].forEach(t=>
{ if( t.index > l )
Expand All @@ -107,8 +112,8 @@ tagUid( node )
tt.push(v);
l = t.index+t[0].length;
})
if( l < e.data.length)
tt.push( txt( e.data.substring(l,e.data.length) ));
if( l < s.length)
tt.push( txt( s.substring(l,s.length) ));
if( tt.length )
{ for( let t of tt )
d.insertBefore(t,e);
Expand Down Expand Up @@ -250,7 +255,7 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
const slotCall = $(xslDom,'call-template[name="slot"]')
, slot2xsl = s =>
{ const v = slotCall.cloneNode(true)
, name = attr(s,'name') || '';
, name = attr(s,'name');
name && v.firstElementChild.setAttribute('select',`'${ name }'`)
for( let c of s.childNodes)
v.lastElementChild.append(c)
Expand Down Expand Up @@ -297,16 +302,20 @@ deepEqual(a, b, O=false)
return O
return true;
}
const splitSliceNames = v => v.split('|').map( s=>s.trim() ).filter(s=>s);

export const
assureSlices = ( root, names) =>
names.split('|').map(n=>n.trim()).map( xp =>
{ if(xp.includes('/'))
{ const ret = [], r = root.ownerDocument.evaluate( xp, root );
splitSliceNames(names).map( xp =>
{ let d = root.ownerDocument
, append = n=> (root.append(n),n);
if(xp.includes('/'))
{ const ret = [], r = d.evaluate( xp, root );
for( let n; n = r.iterateNext(); )
ret.push( n )
return ret
}
return [...root.childNodes].find(n=>n.localName === xp) || create(xp);
return [...root.childNodes].find(n=>n.localName === xp) || append( create(xp,'',d) );
}).flat();

/**
Expand All @@ -319,19 +328,24 @@ assureSlices = ( root, names) =>
export function
event2slice( x, sliceNames, ev, dce )
{
if( ev.sliceProcessed )
return
ev.sliceProcessed = 1;
// evaluate slices[]
// inject @attributes
// inject event
// evaluate slice-value
// slice[i] = slice-value
assureSlices(x,sliceNames).map( s =>
return assureSlices( x, sliceNames ?? '' ).map( s =>
{
const d = x.ownerDocument
, el = ev.sliceEventSource
, sel = ev.sliceElement
, cleanSliceValue = ()=>[...s.childNodes].filter(n=>n.nodeType===3 || n.localName==='value').map(n=>n.remove());
, cleanSliceValue = ()=>[...s.childNodes].filter(n=>n.nodeType===3 || n.localName==='value' || n.localName==='form-data').map(n=>n.remove());
el.getAttributeNames().map( a => s.setAttribute( a, attr(el,a) ) );
[...s.childNodes].filter(n=>n.localName==='event').map(n=>n.remove());
if( 'validationMessage' in el )
s.setAttribute('validation-message', el.validationMessage);
ev.type==='init' && cleanSliceValue();
s.append( obj2node( ev, 'event', d ) );
if( sel.hasAttribute('slice-value') )
Expand All @@ -343,7 +357,12 @@ event2slice( x, sliceNames, ev, dce )
cleanSliceValue();
s.append( createText( d, v ) );
}else
{ const v = el.value ?? attr( sel, 'value' ) ;
{ if( 'elements' in el )
{ cleanSliceValue();
s.append( obj2node(new FormData(el),'value', s.ownerDocument) )
return s
}
const v = el.value ?? attr( sel, 'value' ) ;
cleanSliceValue();
if( v === null || v === undefined )
[...s.childNodes].filter(n=>n.localName!=='event').map(n=>n.remove());
Expand All @@ -353,14 +372,14 @@ event2slice( x, sliceNames, ev, dce )
else
s.append( obj2node(v,'value',s.ownerDocument) )
}
return s
})
}

function forEach$( el, css, cb){
if( el.querySelectorAll )
[...el.querySelectorAll(css)].forEach(cb)
}
const getByHashId = ( n, id )=> ( p => n===p? null: (p && ( p.querySelector(id) || getByHashId(p,id) ) ))( n.getRootNode() )
const loadTemplateRoots = async ( src, dce )=>
{
if( !src || !src.trim() )
Expand Down Expand Up @@ -388,12 +407,7 @@ const loadTemplateRoots = async ( src, dce )=>
}catch (error){ return [dce]}
}
export function mergeAttr( from, to )
{ if( isText(from) )
{
if( !isText(to) ){ debugger }
return
}
for( let a of from.attributes)
{ for( let a of from.attributes)
{ a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
if( a.name === 'value')
to.value = a.value
Expand Down Expand Up @@ -472,12 +486,18 @@ export const xPathDefaults = x=>
// return xx.length ? `${a}|(${xPathDefaults(xx.join('??'))})[not(${a})]`: a
}
export const xPath = (x,root)=>
{ x = xPathDefaults(x);
{
const xx = x.split('??');
if( xx.length > 1 )
return xPath(xx[0], root) || xPath(xx[1], root);

x = xPathDefaults(x);

const it = root.ownerDocument.evaluate(x, root);
switch( it.resultType )
{ case XPathResult.NUMBER_TYPE: return it.numberValue;
case XPathResult.STRING_TYPE: return it.stringValue;
case XPathResult.BOOLEAN_TYPE: return it.booleanValue;
}

let ret = '';
Expand Down Expand Up @@ -529,7 +549,10 @@ CustomElement extends HTMLElement

const dce = this
, sliceNodes = [...this.templateNode.querySelectorAll('[slice]')]
, sliceNames = sliceNodes.map(e=>attr(e,'slice')).filter(n=>!n.includes('/')).filter((v, i, a)=>a.indexOf(v) === i)
, sliceNames = sliceNodes.map(e=>attr(e,'slice'))
.filter(n=>!n.includes('/'))
.filter((v, i, a)=>a.indexOf(v) === i)
.map(splitSliceNames).flat()
, declaredAttributes = templateDocs.reduce( (ret,t) => { if( t.params ) ret.push( ...t.params ); return ret; }, [] );

class DceElement extends HTMLElement
Expand All @@ -540,6 +563,8 @@ CustomElement extends HTMLElement
{ let payload = this.childNodes;
if( this.firstElementChild?.tagName === 'TEMPLATE' )
{
if( this.firstElementChild !== this.lastElementChild )
{ console.error('payload should have TEMPLATE as only child', this.outerHTML ) }
const t = this.firstElementChild;
t.remove();
payload = t.content.childNodes;
Expand All @@ -563,7 +588,7 @@ CustomElement extends HTMLElement
})(x.ownerDocument.createElement( tag ))
injectData( x, 'payload' , payload , assureSlot );
this.innerHTML='';
injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
const attrsRoot = injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
injectData( x, 'dataset', Object.keys( this.dataset ), k => createXmlNode( k, this.dataset[ k ] ) );
const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) )
, sliceXPath = x => xPath(x, sliceRoot);
Expand All @@ -585,14 +610,12 @@ CustomElement extends HTMLElement
let timeoutID;

this.onSlice = ev=>
{ ev.stopPropagation?.();
ev.sliceEventSource = ev.currentTarget || ev.target;
sliceEvents.push(ev);
{ sliceEvents.push(ev);
if( !timeoutID )
timeoutID = setTimeout(()=>
{ applySlices();
timeoutID =0;
},10);
},1);
};
const transform = this.transform = ()=>
{ if(this.#inTransform){ debugger }
Expand All @@ -619,20 +642,58 @@ CustomElement extends HTMLElement
}
})

forEach$( this,'[slice]', el =>
forEach$( this,'[slice],[slice-event]', el =>
{ if( !el.dceInitialized )
{ el.dceInitialized = 1;
const evs = attr(el,'slice-event');
(evs || 'change')
.split(' ')
let evs = attr(el,'slice-event');
if( attr(el,'custom-validity') )
evs += ' change submit';

[...new Set((evs || 'change') .split(' '))]
.forEach( t=> (el.localName==='slice'? el.parentElement : el)
.addEventListener( t, ev=>
{ ev.sliceElement = el;
this.onSlice(ev)
} ));
.addEventListener( t, ev=>
{ ev.sliceElement = el;
ev.sliceEventSource = ev.currentTarget || ev.target;
const slices = event2slice( sliceRoot, attr( ev.sliceElement, 'slice'), ev, this );

forEach$(this,'[custom-validity]',el =>
{ if( !el.setCustomValidity )
return;
const x = attr( el, 'custom-validity' );
try
{ const v = x && xPath( x, attrsRoot );
el.setCustomValidity( v === true? '': v === false ? 'invalid' : v );
}catch(err)
{ console.error(err, 'xPath', x) }
})
const x = attr(el,'custom-validity')
, v = x && xPath( x, attrsRoot )
, msg = v === true? '' : v;

if( x )
{ el.setCustomValidity ? el.setCustomValidity( msg ) : ( el.validationMessage = msg );
slices.map( s => s.setAttribute('validation-message', msg ) );
if( ev.type === 'submit' )
{ if( v === true )
return;
setTimeout(transform,1)
if( !!v === v )
{ v || ev.preventDefault();
return v;
}
if( v )
{ ev.preventDefault();
return !1
}
return ;
}else
setTimeout(transform,1)
}
this.onSlice(ev);
} ));
if( !evs || evs.includes('init') )
{ if( el.hasAttribute('slice-value') || el.hasAttribute('value') || el.value )
this.onSlice({type:'init', target: el, sliceElement:el })
this.onSlice({type:'init', target: el, sliceElement:el, sliceEventSource:el })
else
el.value = sliceXPath( attr(el,'slice') )
}
Expand Down
32 changes: 32 additions & 0 deletions demo/data-slices.html
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,38 @@ <h3>Data slices propagation by events.</h3>
</template>
</html-demo-element>


<html-demo-element legend="10. multiple slices by same field"
description="same element value sets s1 and s2 slice">
<template>
<custom-element>
<template>
<input slice="s1|s2"
slice-event="input"
data-testid="f1"
/><br/>
Type to update s1 and s2 slices <br/>
slice <code>s1: {//slice/s1}</code><br/>
slice <code>s2: {//slice/s2}</code><br/>
</template>
</custom-element>
</template>
</html-demo-element>

<html-demo-element legend="11. slices and attribute"
description="initial attribute value should be smile as emoji and :) on blur from input it should be updated from value">
<template>
<custom-element>
<template>
<attribute name="emotion">😃</attribute>
<input slice="/datadom/attributes/emotion | s1"/>
Type and unfocus to update emotion attribute: {emotion}
and slice: {//slice/s1}
</template>
</custom-element>
</template>
</html-demo-element>

<script type="module" src="https://unpkg.com/html-demo-element@1/html-demo-element.js"></script>

</body>
Expand Down
Loading

0 comments on commit 7e1249c

Please sign in to comment.