@epa-wg/custom-element 0.0.7 → 0.0.8
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/.idea/custom-element.iml +9 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +48 -3
- package/custom-element.js +116 -22
- package/datasource.md +64 -0
- package/demo/demo.css +19 -0
- package/demo/http-request.html +61 -0
- package/demo/local-storage.html +103 -0
- package/http-request.js +44 -0
- package/index.html +8 -6
- package/local-storage.js +66 -0
- package/package.json +2 -1
- package/request.html +53 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="JAVA_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="inheritedJdk" />
|
|
7
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|
package/.idea/misc.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/custom-element.iml" filepath="$PROJECT_DIR$/.idea/custom-element.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/vcs.xml
ADDED
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ It allows to define custom HTML tag with template filled from slots and attribut
|
|
|
8
8
|
| Live demo: [custom-element][demo-url]
|
|
9
9
|
| Try in [Sandbox][sandbox-url]
|
|
10
10
|
| [tests project][git-test-url]
|
|
11
|
+
| [Chrome devtools pugin][plugin-url]
|
|
11
12
|
|
|
12
13
|
[![NPM version][npm-image]][npm-url]
|
|
13
14
|
[![coverage][coverage-image]][coverage-url]
|
|
@@ -127,7 +128,50 @@ is available in `{}` in attributes, in `xsl:for-each`, `xsl:if`, `xsl:value-of`,
|
|
|
127
128
|
|
|
128
129
|
XPath is a selector language to navigate over custom element instance data, attributes, and payload.
|
|
129
130
|
|
|
131
|
+
## XSLT 1.0
|
|
132
|
+
The in-browser native implementation as of now supports [XSLT 1.0](https://www.w3.org/TR/xslt-10/).
|
|
133
|
+
File the [change request](https://github.com/EPA-WG/custom-element/issues) for support of another XSLT version or
|
|
134
|
+
template engine.
|
|
135
|
+
|
|
130
136
|
# troubleshooting
|
|
137
|
+
## HTML parser is not compatible with templates
|
|
138
|
+
On many tags like `table`, or link `a` the attempt to use XSLT operations could lead to DOM order missmatch to given
|
|
139
|
+
in template. In such cases the `html:` prefix in front of troubled tag would solve the parsing.
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<custom-element tag="dce-2" hidden>
|
|
143
|
+
<local-storage key="basket" slice="basket"></local-storage>
|
|
144
|
+
<html:table>
|
|
145
|
+
<xsl:for-each select="//slice/basket/@*">
|
|
146
|
+
<html:tr>
|
|
147
|
+
<html:th><xsl:value-of select="name()"/></html:th>
|
|
148
|
+
<html:td><xsl:value-of select="."/></html:td>
|
|
149
|
+
</html:tr>
|
|
150
|
+
</xsl:for-each>
|
|
151
|
+
</html:table>
|
|
152
|
+
count:<xsl:value-of select="count(//slice/basket/@*)"/>
|
|
153
|
+
</custom-element>
|
|
154
|
+
```
|
|
155
|
+
See [demo source](demo/local-storage.html) for detailed sample.
|
|
156
|
+
|
|
157
|
+
## Chrome devtools plugin
|
|
158
|
+
[@epa-wg/custom-element plugin][plugin-url] gives the view into
|
|
159
|
+
|
|
160
|
+
* `current` selected in DOM inspector node
|
|
161
|
+
* Parent `customElement`
|
|
162
|
+
* Declarative Custom Element `dce` for custom element ^^
|
|
163
|
+
|
|
164
|
+
* `datadom` for easier inspection
|
|
165
|
+
* `xml` as a string
|
|
166
|
+
* `xslt` as a string
|
|
167
|
+
|
|
168
|
+
## template debugging
|
|
169
|
+
`xml` and `xslt` can be saved to file via for "_copy string contents_" into clipboard.
|
|
170
|
+
|
|
171
|
+
The XSLT debugger from your favorite IDE can set the breakpoints withing those files and
|
|
172
|
+
run transformation under debugger.
|
|
173
|
+
|
|
174
|
+
|
|
131
175
|
## `{}` does not give a value
|
|
132
176
|
* try to add as attribute you could observe and put the value of node name or text to identify the current location in data
|
|
133
177
|
within template
|
|
@@ -141,9 +185,10 @@ within template
|
|
|
141
185
|
[github-image]: https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/mark-github.svg
|
|
142
186
|
[npm-image]: https://img.shields.io/npm/v/@epa-wg/custom-element.svg
|
|
143
187
|
[npm-url]: https://npmjs.org/package/@epa-wg/custom-element
|
|
144
|
-
[coverage-image]: https://unpkg.com/@epa-wg/custom-element-test@0.0.
|
|
145
|
-
[coverage-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.
|
|
146
|
-
[storybook-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.
|
|
188
|
+
[coverage-image]: https://unpkg.com/@epa-wg/custom-element-test@0.0.8/coverage/coverage.svg
|
|
189
|
+
[coverage-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.8/coverage/lcov-report/index.html
|
|
190
|
+
[storybook-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.8/storybook-static/index.html?path=/story/welcome--introduction
|
|
147
191
|
[sandbox-url]: https://stackblitz.com/github/EPA-WG/custom-element?file=index.html
|
|
148
192
|
[webcomponents-url]: https://www.webcomponents.org/element/@epa-wg/custom-element
|
|
149
193
|
[webcomponents-img]: https://img.shields.io/badge/webcomponents.org-published-blue.svg
|
|
194
|
+
[plugin-url]: https://chrome.google.com/webstore/detail/epa-wgcustom-element/hiofgpmmkdembdogjpagmbbbmefefhbl
|
package/custom-element.js
CHANGED
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
2
|
-
|
|
2
|
+
, XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform';
|
|
3
3
|
|
|
4
4
|
// const log = x => console.debug( new XMLSerializer().serializeToString( x ) );
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const e = document.createElement( tag );
|
|
9
|
-
if( t ) e.innerText = t;
|
|
10
|
-
return e;
|
|
11
|
-
}
|
|
6
|
+
const attr = (el, attr)=> el.getAttribute(attr)
|
|
7
|
+
, create = ( tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElement( tag ));
|
|
12
8
|
|
|
13
|
-
function
|
|
9
|
+
function
|
|
10
|
+
xml2dom( xmlString )
|
|
14
11
|
{
|
|
15
12
|
return new DOMParser().parseFromString( XML_DECLARATION + xmlString, "application/xml" )
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
function
|
|
15
|
+
function
|
|
16
|
+
bodyXml( dce )
|
|
19
17
|
{
|
|
18
|
+
const t = dce.firstElementChild
|
|
19
|
+
, sanitize = s => s.replaceAll("<html:","<")
|
|
20
|
+
.replaceAll("</html:","</");
|
|
21
|
+
if( t?.tagName === 'TEMPLATE')
|
|
22
|
+
return sanitize( new XMLSerializer().serializeToString( t.content ) );
|
|
23
|
+
|
|
20
24
|
const s = new XMLSerializer().serializeToString( dce );
|
|
21
|
-
return s.substring( s.indexOf( '>' ) + 1, s.lastIndexOf( '<' ) );
|
|
25
|
+
return sanitize( s.substring( s.indexOf( '>' ) + 1, s.lastIndexOf( '<' ) ) );
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
function
|
|
28
|
+
function
|
|
29
|
+
slot2xsl( s )
|
|
25
30
|
{
|
|
26
31
|
const v = document.createElementNS( XSL_NS_URL, 'value-of' );
|
|
27
32
|
v.setAttribute( 'select', `//*[@slot="${ s.name }"]` );
|
|
28
33
|
s.parentNode.replaceChild( v, s );
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
function
|
|
36
|
+
function
|
|
37
|
+
injectData( root, sectionName, arr, cb )
|
|
32
38
|
{
|
|
33
39
|
const inject = ( tag, parent, s ) =>
|
|
34
40
|
{
|
|
@@ -37,9 +43,11 @@ function injectData( root, sectionName, arr, cb )
|
|
|
37
43
|
};
|
|
38
44
|
const l = inject( sectionName, root );
|
|
39
45
|
[ ...arr ].forEach( e => l.append( cb( e ) ) );
|
|
46
|
+
return l;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
function
|
|
49
|
+
function
|
|
50
|
+
assureSlot( e )
|
|
43
51
|
{
|
|
44
52
|
if( !e.slot )
|
|
45
53
|
{
|
|
@@ -50,17 +58,61 @@ function assureSlot( e )
|
|
|
50
58
|
return e;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
export
|
|
61
|
+
export function
|
|
62
|
+
Json2Xml( o, tag )
|
|
63
|
+
{
|
|
64
|
+
if( typeof o === 'string' )
|
|
65
|
+
return o;
|
|
66
|
+
|
|
67
|
+
const noTag = "string" != typeof tag;
|
|
68
|
+
|
|
69
|
+
if( o instanceof Array )
|
|
70
|
+
{ noTag && (tag = 'array');
|
|
71
|
+
return "<"+tag+">"+o.map(function(el){ return Json2Xml(el,tag); }).join()+"</"+tag+">";
|
|
72
|
+
}
|
|
73
|
+
noTag && (tag = 'r');
|
|
74
|
+
tag=tag.replace( /[^a-z0-9]/gi,'_' );
|
|
75
|
+
var oo = {}
|
|
76
|
+
, ret = [ "<"+tag+" "];
|
|
77
|
+
for( var k in o )
|
|
78
|
+
if( typeof o[k] == "object" )
|
|
79
|
+
oo[k] = o[k];
|
|
80
|
+
else
|
|
81
|
+
ret.push( k.replace( /[^a-z0-9]/gi,'_' ) + '="'+o[k].toString().replace(/&/gi,'&')+'"');
|
|
82
|
+
if( oo )
|
|
83
|
+
{ ret.push(">");
|
|
84
|
+
for( var k in oo )
|
|
85
|
+
ret.push( Json2Xml( oo[k], k ) );
|
|
86
|
+
ret.push("</"+tag+">");
|
|
87
|
+
}else
|
|
88
|
+
ret.push("/>");
|
|
89
|
+
return ret.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function
|
|
93
|
+
injectSlice( x, s, data )
|
|
94
|
+
{
|
|
95
|
+
const el = create(s)
|
|
96
|
+
, isString = typeof data === 'string' ;
|
|
97
|
+
el.innerHTML = isString? data : Json2Xml( data, s );
|
|
98
|
+
const slice = isString? el : el.firstChild;
|
|
99
|
+
[...x.children].filter( e=>e.localName === s ).map( el=>el.remove() );
|
|
100
|
+
x.append(slice);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class
|
|
104
|
+
CustomElement extends HTMLElement
|
|
54
105
|
{
|
|
55
106
|
constructor()
|
|
56
107
|
{
|
|
57
108
|
super();
|
|
58
109
|
|
|
59
|
-
[ ...this.
|
|
110
|
+
[ ...this.templateNode.querySelectorAll('slot') ].forEach( slot2xsl );
|
|
60
111
|
const p = new XSLTProcessor();
|
|
61
112
|
p.importStylesheet( this.xslt );
|
|
62
|
-
const tag =
|
|
113
|
+
const tag = attr( this, 'tag' );
|
|
63
114
|
const dce = this;
|
|
115
|
+
const sliceNames = [...this.templateNode.querySelectorAll('[slice]')].map(e=>attr(e,'slice'));
|
|
64
116
|
tag && window.customElements.define( tag, class extends HTMLElement
|
|
65
117
|
{
|
|
66
118
|
constructor()
|
|
@@ -70,18 +122,59 @@ export class CustomElement extends HTMLElement
|
|
|
70
122
|
injectData( x, 'payload', this.childNodes, assureSlot );
|
|
71
123
|
injectData( x, 'attributes', this.attributes, e => create( e.nodeName, e.value ) );
|
|
72
124
|
injectData( x, 'dataset', Object.keys( this.dataset ), k => create( k, this.dataset[ k ] ) );
|
|
125
|
+
const sliceRoot = injectData( x, 'slice', sliceNames, k => create( k, '' ) );
|
|
73
126
|
this.xml = x;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
127
|
+
const slices = {};
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
const sliceEvents=[];
|
|
131
|
+
const applySlices = ()=>
|
|
132
|
+
{ const processed = {}
|
|
133
|
+
|
|
134
|
+
for(let ev; ev = sliceEvents.pop(); )
|
|
135
|
+
{ const s = attr( ev.target, 'slice');
|
|
136
|
+
if( processed[s] )
|
|
137
|
+
continue;
|
|
138
|
+
injectSlice( sliceRoot, s, ev.detail );
|
|
139
|
+
processed[s] = ev;
|
|
140
|
+
}
|
|
141
|
+
Object.keys(processed).length !== 0 && transform();
|
|
142
|
+
}
|
|
143
|
+
let timeoutID;
|
|
144
|
+
|
|
145
|
+
const onSlice = ev=>
|
|
146
|
+
{ ev.stopPropagation?.();
|
|
147
|
+
sliceEvents.push(ev);
|
|
148
|
+
if( !timeoutID )
|
|
149
|
+
timeoutID = setTimeout(()=>
|
|
150
|
+
{ applySlices();
|
|
151
|
+
timeoutID =0;
|
|
152
|
+
},10);
|
|
153
|
+
};
|
|
154
|
+
this.onSlice = onSlice;
|
|
155
|
+
const transform = ()=>
|
|
156
|
+
{
|
|
157
|
+
const f = p.transformToFragment( x, document );
|
|
158
|
+
this.innerHTML = '';
|
|
159
|
+
[ ...f.childNodes ].forEach( e => this.appendChild( e ) );
|
|
160
|
+
|
|
161
|
+
for( let el of this.querySelectorAll('[slice]') )
|
|
162
|
+
if( 'function' === typeof el.sliceInit )
|
|
163
|
+
{ const s = attr(el,'slice');
|
|
164
|
+
slices[s] = el.sliceInit( slices[s] );
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
transform();
|
|
168
|
+
applySlices();
|
|
77
169
|
}
|
|
78
170
|
get dce(){ return dce;}
|
|
79
171
|
} );
|
|
80
172
|
}
|
|
173
|
+
get templateNode(){ return this.firstElementChild?.tagName === 'TEMPLATE'? this.firstElementChild.content : this }
|
|
81
174
|
get dce(){ return this;}
|
|
82
|
-
get
|
|
175
|
+
get xsltString()
|
|
83
176
|
{
|
|
84
|
-
return
|
|
177
|
+
return (
|
|
85
178
|
`<xsl:stylesheet version="1.0"
|
|
86
179
|
xmlns:xsl="${ XSL_NS_URL }">
|
|
87
180
|
<xsl:output method="html" />
|
|
@@ -95,7 +188,8 @@ export class CustomElement extends HTMLElement
|
|
|
95
188
|
|
|
96
189
|
</xsl:stylesheet>` );
|
|
97
190
|
}
|
|
191
|
+
get xslt(){ return xml2dom( this.xsltString ); }
|
|
98
192
|
}
|
|
99
193
|
|
|
100
194
|
window.customElements.define( 'custom-element', CustomElement );
|
|
101
|
-
export default CustomElement;
|
|
195
|
+
export default CustomElement;
|
package/datasource.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<h1>DRAFT</h1>
|
|
2
|
+
|
|
3
|
+
# Data Sourse (DS) and transformation pipeline
|
|
4
|
+
|
|
5
|
+
The DS is the data provider for template and DCE. It defines the way to retrieve the data, does the data fetch and
|
|
6
|
+
notifies the template owner on data availability.
|
|
7
|
+
# DS types
|
|
8
|
+
The data samples which are needed for Declarative Web Application would include
|
|
9
|
+
* remote data available over HTTP from URL, request parameters, and HTTP headers
|
|
10
|
+
* embedded into page data island(s) available over `#` anchors
|
|
11
|
+
* variety of storages including localStorage/sesionStorage
|
|
12
|
+
* web application properties like URI, app settings, import maps, etc.
|
|
13
|
+
|
|
14
|
+
# DS Life cycle
|
|
15
|
+
## 1. Declaration
|
|
16
|
+
resides within template with the parameters populated by expression from template owner data.
|
|
17
|
+
```html
|
|
18
|
+
<custom-element>
|
|
19
|
+
<local-storage key="{app/url/host}/key1" name="slice1"></local-storage>
|
|
20
|
+
</custom-element>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 2. DataRequest Rendering iterations
|
|
24
|
+
DS initiated as DataRequest(**DR**) by [load](https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event)
|
|
25
|
+
event. It starts its lifecycle as async process which emitted to itself as "process" event which bubbles uo to template holder.
|
|
26
|
+
|
|
27
|
+
The process event passes 3 parameters:
|
|
28
|
+
* DS name
|
|
29
|
+
* data
|
|
30
|
+
* state
|
|
31
|
+
|
|
32
|
+
Initially the state is "initiated" with default or no data, then "progress" with partial data, and in the end "completed" with final data.
|
|
33
|
+
Process owner (template renderer) would populate the passed data slices into dataset and mark itself as "dirty" , i.e candidate for re-render upon finalizing the "pre-render" phase.
|
|
34
|
+
|
|
35
|
+
## 3. final transformation
|
|
36
|
+
The DS can be instant routine which immediately return the final state. Sample of such is app settings.
|
|
37
|
+
|
|
38
|
+
DS with finite steps is any remote call.
|
|
39
|
+
|
|
40
|
+
DS which is never ending samples are localStorage, application URL, clipboard, etc.
|
|
41
|
+
|
|
42
|
+
The transformation owner (DCE, include, etc.) would track the Data Request state change and keep re-rendering on each
|
|
43
|
+
state change. This DR state change notification is the custom event which bubbles up to the transformation owner.
|
|
44
|
+
|
|
45
|
+
# Browser events vs API
|
|
46
|
+
In environments where the data sources custom elements and lifecycle events are not available, the transformation 'owner'
|
|
47
|
+
would be able to track data sources and generate new stream for each data state change. The following sequence of
|
|
48
|
+
data loading and transformations would work even for PDF rendering.
|
|
49
|
+
|
|
50
|
+
1. transformation process (TP) would start the XSLT processing
|
|
51
|
+
2. on DS branch it would
|
|
52
|
+
* check the DS state by unique id(XPath?) in the template state.
|
|
53
|
+
* if DS not yet initialized,
|
|
54
|
+
* create the data request(DR) object
|
|
55
|
+
* DR is a process which continue to live in own thread and starts data retrieval.
|
|
56
|
+
* DR, provides its state and data to transformation via data slice
|
|
57
|
+
* DS data slice is set on the TP by the name with initial default value
|
|
58
|
+
* notify the TR to mark the DR `incomplete`
|
|
59
|
+
* finish transformation with blank data
|
|
60
|
+
3. when DR receives data or state change, it would notify the TR to mark the DR `incomplete`
|
|
61
|
+
4. TP would re-trigger the transformation on each DR state change.
|
|
62
|
+
5. the transformation with all DRs in final state(error or completed) is counted as last.
|
|
63
|
+
It is up to implementation to break the current transformation when the any of DR status changes.
|
|
64
|
+
Depend of application, either final or the sequence of rendered transforms could be used.
|
package/demo/demo.css
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
html
|
|
2
|
+
{ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
|
|
3
|
+
font-weight: 400; font-style: normal; -webkit-font-smoothing: antialiased;
|
|
4
|
+
}
|
|
5
|
+
body,nav{ display: flex; flex-wrap: wrap; align-content: stretch; gap: 1rem; }
|
|
6
|
+
body>*{flex: auto;}
|
|
7
|
+
nav{ flex-direction: column;}
|
|
8
|
+
dce-link,dce-1-slot,dce-2-slot,dce-3-slot,dce-4-slot,dce-2-slots,greet-element,pokemon-tile,
|
|
9
|
+
dce-1,dce-2,dce-3,dce-4
|
|
10
|
+
{ box-shadow: 0 0 0.5rem lime; padding: 1rem; display: inline-block; flex:1; }
|
|
11
|
+
dd{ padding: 1rem;}
|
|
12
|
+
p{ margin: 0;}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
html-demo-element h3
|
|
16
|
+
{ text-shadow: 0 0 0.25em white;
|
|
17
|
+
letter-spacing: 0.1rem;
|
|
18
|
+
}
|
|
19
|
+
*[slot="demo"]{ display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:html="http://www.w3.org/1999/xhtml">
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
5
|
+
<title>http-request Declarative Custom Element implementation demo</title>
|
|
6
|
+
<script type="module" src="../http-request.js"></script>
|
|
7
|
+
<script type="module" src="../custom-element.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
@import "./demo.css";
|
|
10
|
+
|
|
11
|
+
button
|
|
12
|
+
{ display: inline-flex; flex-direction: column; align-items: center; flex: auto;
|
|
13
|
+
box-shadow: inset silver 0px 0px 1rem; min-width: 12rem; padding: 1rem;
|
|
14
|
+
color: coral; text-shadow: 1px 1px silver; font-weight: bolder;
|
|
15
|
+
}
|
|
16
|
+
button img{ max-height: 10vw; min-height: 4rem;}
|
|
17
|
+
table{ min-width: 16rem; }
|
|
18
|
+
td{ border-bottom: 1px solid silver; }
|
|
19
|
+
tfoot td{ border-bottom: none; }
|
|
20
|
+
td,th{text-align: right; }
|
|
21
|
+
caption{ padding: 1rem; font-weight: bolder; font-family: sans-serif; }
|
|
22
|
+
dce-1{ padding: 0; display: flex; flex-wrap: wrap;}
|
|
23
|
+
code{ text-align: right; min-width: 3rem;}
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
<html-demo-element legend="1. http-request simplest"
|
|
30
|
+
description="load the list of pokemons">
|
|
31
|
+
<p>Should display 6 image buttons with pokemon name </p>
|
|
32
|
+
<template>
|
|
33
|
+
<custom-element tag="dce-1" hidden>
|
|
34
|
+
<template><!-- wrapping into template to prevent images loading within DCE declaration -->
|
|
35
|
+
<http-request
|
|
36
|
+
url="https://pokeapi.co/api/v2/pokemon?limit=6&offset=0"
|
|
37
|
+
slice="page"
|
|
38
|
+
></http-request>
|
|
39
|
+
<xsl:for-each select="//slice/page/data/results/*">
|
|
40
|
+
<xsl:variable name="slides-url"
|
|
41
|
+
>https://unpkg.com/pokeapi-sprites@2.0.2/sprites/pokemon/other/dream-world</xsl:variable>
|
|
42
|
+
<xsl:variable name="pokeid"
|
|
43
|
+
select="substring-before( substring-after( @url, 'https://pokeapi.co/api/v2/pokemon/'),'/')"
|
|
44
|
+
></xsl:variable>
|
|
45
|
+
<button>
|
|
46
|
+
<img src="{$slides-url}/{$pokeid}.svg"
|
|
47
|
+
alt="{@name}"/>
|
|
48
|
+
<xsl:value-of select='@name'/>
|
|
49
|
+
</button>
|
|
50
|
+
</xsl:for-each>
|
|
51
|
+
</template>
|
|
52
|
+
</custom-element>
|
|
53
|
+
<dce-1></dce-1>
|
|
54
|
+
</template>
|
|
55
|
+
</html-demo-element>
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
<script type="module" src="https://unpkg.com/html-demo-element@1/html-demo-element.js"></script>
|
|
59
|
+
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:html="http://www.w3.org/1999/xhtml">
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
5
|
+
<title>custom-element Declarative Custom Element implementation demo</title>
|
|
6
|
+
<script type="module" src="../local-storage.js"></script>
|
|
7
|
+
<script type="module" src="../custom-element.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
@import "./demo.css";
|
|
10
|
+
|
|
11
|
+
button{ background: forestgreen; }
|
|
12
|
+
table{ min-width: 16rem; }
|
|
13
|
+
td{ border-bottom: 1px solid silver; }
|
|
14
|
+
tfoot td{ border-bottom: none; }
|
|
15
|
+
td,th{text-align: right; }
|
|
16
|
+
caption{ padding: 1rem; font-weight: bolder; font-family: sans-serif; }
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
<html-demo-element legend="1. localStorage simplest"
|
|
23
|
+
description="local-storage read only during initial and only render, does not track the changes.">
|
|
24
|
+
<p>Has to produce 12🍒</p>
|
|
25
|
+
<template>
|
|
26
|
+
<custom-element tag="dce-1" hidden>
|
|
27
|
+
<xsl:value-of select="//slice/fruits/text()"></xsl:value-of>
|
|
28
|
+
<slot>🤔</slot>
|
|
29
|
+
<local-storage key="cherries" slice="fruits"></local-storage>
|
|
30
|
+
</custom-element>
|
|
31
|
+
<dce-1>🍒</dce-1>
|
|
32
|
+
</template>
|
|
33
|
+
</html-demo-element>
|
|
34
|
+
|
|
35
|
+
<html-demo-element legend="2. localStorage basket JSON "
|
|
36
|
+
description="local-storage tracks changes">
|
|
37
|
+
<p>Click the fruits button to add into cart </p>
|
|
38
|
+
<template>
|
|
39
|
+
<custom-element tag="dce-2" hidden>
|
|
40
|
+
<local-storage key="basket" slice="basket" live type="json"></local-storage>
|
|
41
|
+
<html:table>
|
|
42
|
+
<xsl:for-each select="//slice/basket/@*">
|
|
43
|
+
<html:tr>
|
|
44
|
+
<html:th><xsl:value-of select="name()"/></html:th>
|
|
45
|
+
<html:td><xsl:value-of select="."/></html:td>
|
|
46
|
+
</html:tr>
|
|
47
|
+
</xsl:for-each>
|
|
48
|
+
<html:tfoot>
|
|
49
|
+
<html:tr>
|
|
50
|
+
<html:td><slot>🤔</slot></html:td>
|
|
51
|
+
<html:th><xsl:value-of select="sum(//slice/basket/@*)"/></html:th>
|
|
52
|
+
</html:tr>
|
|
53
|
+
</html:tfoot>
|
|
54
|
+
</html:table>
|
|
55
|
+
</custom-element>
|
|
56
|
+
<dce-2>🛒total</dce-2>
|
|
57
|
+
</template>
|
|
58
|
+
</html-demo-element>
|
|
59
|
+
|
|
60
|
+
<fieldset>
|
|
61
|
+
<legend>localStorage content</legend>
|
|
62
|
+
<p>The demo should display count 1🍋 and 12🍒 initially.
|
|
63
|
+
The value in <code>localStorage</code> is incremented
|
|
64
|
+
when clicked on matching button
|
|
65
|
+
</p>
|
|
66
|
+
<button name="lemons" value="1" >🍋</button>
|
|
67
|
+
<button name="cherries" value="12" >🍒</button>
|
|
68
|
+
<button name="apple" >🍏</button>
|
|
69
|
+
<button name="banana" >🍌</button>
|
|
70
|
+
<table>
|
|
71
|
+
<caption> Click to add the localStorage value </caption>
|
|
72
|
+
<thead><tr><th>key</th><th>value</th></tr></thead>
|
|
73
|
+
<tbody id="local-storage-values"></tbody>
|
|
74
|
+
</table>
|
|
75
|
+
</fieldset>
|
|
76
|
+
<script type="module">
|
|
77
|
+
import $ from 'https://unpkg.com/css-chain@1/CssChain.js';
|
|
78
|
+
|
|
79
|
+
const basket = {cherries: 12, lemons:1 };
|
|
80
|
+
localStorage.setItem( 'basket', JSON.stringify(basket) );
|
|
81
|
+
|
|
82
|
+
$('button[name]')
|
|
83
|
+
.forEach( b=> localStorage.setItem( b.name, b.value ) )
|
|
84
|
+
.addEventListener( 'click', e =>
|
|
85
|
+
{ const k = e.target.name;
|
|
86
|
+
basket[k] || (basket[k] = 1);
|
|
87
|
+
localStorage.setItem( k, basket[k] = 1+1*localStorage[k] )
|
|
88
|
+
localStorage.setItem( 'basket', JSON.stringify(basket) );
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
const renderStorage = () =>
|
|
92
|
+
window[ 'local-storage-values' ].innerHTML = [...Array(localStorage.length).keys()]
|
|
93
|
+
.map( k => `<tr><th>${ localStorage.key(k) }</th><td>${ localStorage.getItem( localStorage.key(k) ) }</td>` ).join( '\n' );
|
|
94
|
+
|
|
95
|
+
window.addEventListener( 'storage', renderStorage );
|
|
96
|
+
window.addEventListener( 'local-storage', renderStorage );
|
|
97
|
+
renderStorage();
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<script type="module" src="https://unpkg.com/html-demo-element@1/html-demo-element.js"></script>
|
|
101
|
+
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
package/http-request.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const attr = (el, attr)=> el.getAttribute(attr);
|
|
2
|
+
|
|
3
|
+
export class HttpRequestElement extends HTMLElement
|
|
4
|
+
{
|
|
5
|
+
// @attribute url
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
}
|
|
9
|
+
sliceInit( s )
|
|
10
|
+
{ if( !s )
|
|
11
|
+
s = {};
|
|
12
|
+
s.element = this;
|
|
13
|
+
if( s.destroy )
|
|
14
|
+
return s;
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
s.destroy = ()=>
|
|
17
|
+
{ // todo destroy slices in custom-element
|
|
18
|
+
controller.abort();
|
|
19
|
+
};
|
|
20
|
+
const url = attr(this, 'url') || ''
|
|
21
|
+
, request = { url }
|
|
22
|
+
, slice = { detail: { request }, target: this }
|
|
23
|
+
, updateSlice = slice =>
|
|
24
|
+
{ for( let parent = s.element.parentElement; parent; parent = parent.parentElement )
|
|
25
|
+
if ( parent.onSlice )
|
|
26
|
+
return parent.onSlice(slice);
|
|
27
|
+
console.error(`${this.localName} used outside of custom-element`)
|
|
28
|
+
debugger;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
setTimeout( async ()=>
|
|
32
|
+
{ updateSlice( slice );
|
|
33
|
+
slice.detail.response = await fetch(url,{ signal: controller.signal });
|
|
34
|
+
updateSlice( slice );
|
|
35
|
+
slice.detail.data = await slice.detail.response.json();
|
|
36
|
+
updateSlice( slice );
|
|
37
|
+
},0 );
|
|
38
|
+
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
window.customElements.define( 'http-request', HttpRequestElement );
|
|
44
|
+
export default HttpRequestElement;
|
package/index.html
CHANGED
|
@@ -5,12 +5,7 @@
|
|
|
5
5
|
<title>custom-element Declarative Custom Element implementation demo</title>
|
|
6
6
|
<script type="module" src="custom-element.js"></script>
|
|
7
7
|
<style>
|
|
8
|
-
|
|
9
|
-
nav{ flex-direction: column;}
|
|
10
|
-
dce-link,dce-1-slot,dce-2-slot,dce-3-slot,dce-4-slot,dce-2-slots,greet-element,pokemon-tile
|
|
11
|
-
{ box-shadow: 0 0 0.5rem lime; padding: 1rem; display: inline-block;}
|
|
12
|
-
dd{ padding: 1rem;}
|
|
13
|
-
p{ margin: 0;}
|
|
8
|
+
@import "demo/demo.css";
|
|
14
9
|
</style>
|
|
15
10
|
</head>
|
|
16
11
|
<body>
|
|
@@ -19,6 +14,8 @@
|
|
|
19
14
|
<div><a href="https://github.com/EPA-WG/custom-element"
|
|
20
15
|
><img src="https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/mark-github.svg" alt="icon">GIT</a>
|
|
21
16
|
| <a href="https://stackblitz.com/github/EPA-WG/custom-element?file=index.html">Sandbox</a>
|
|
17
|
+
| <a href="https://chrome.google.com/webstore/detail/epa-wgcustom-element/hiofgpmmkdembdogjpagmbbbmefefhbl"
|
|
18
|
+
>Chrome devtools plugin</a>
|
|
22
19
|
</div>
|
|
23
20
|
<p>
|
|
24
21
|
This <em>Declarative Custom Element</em> allows to define<br/>
|
|
@@ -26,6 +23,11 @@
|
|
|
26
23
|
<p>The template is fully loaded with variables, conditions, loops, etc. <br/>
|
|
27
24
|
The data query is powered by XPath. </p>
|
|
28
25
|
<p>Try in <a href="https://stackblitz.com/github/EPA-WG/custom-element?file=index.html" >Sandbox</a> </p>
|
|
26
|
+
<section>
|
|
27
|
+
<b>Data layer demo</b>
|
|
28
|
+
<a href="./demo/local-storage.html">local-storage</a> |
|
|
29
|
+
<a href="./demo/http-request.html">http-request</a>
|
|
30
|
+
</section>
|
|
29
31
|
</nav>
|
|
30
32
|
<html-demo-element legend="1. simple payload"
|
|
31
33
|
description="payload is ignored as in DCE definition there is no default slot">
|
package/local-storage.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const attr = (el, attr)=> el.getAttribute(attr)
|
|
2
|
+
, string2value = (type, v) =>
|
|
3
|
+
{ if( type === 'text')
|
|
4
|
+
return v;
|
|
5
|
+
if( type === 'json')
|
|
6
|
+
return JSON.parse( v );
|
|
7
|
+
const el = document.createElement('input');
|
|
8
|
+
el.setAttribute('type',type);
|
|
9
|
+
el.setAttribute('value', v );
|
|
10
|
+
return type==='number'? el.valueAsNumber : 'date|time|dateTimeLocal'.includes(type)? el.valueAsDate: el.value;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let originalSetItem;
|
|
14
|
+
|
|
15
|
+
function ensureTrackLocalStorage()
|
|
16
|
+
{ if( originalSetItem )
|
|
17
|
+
return;
|
|
18
|
+
originalSetItem = localStorage.setItem;
|
|
19
|
+
localStorage.setItem = function( key, value, ...rest )
|
|
20
|
+
{ originalSetItem.apply(this, [ key, value, ...rest ]);
|
|
21
|
+
window.dispatchEvent( new CustomEvent('local-storage',{detail:{key,value}}) );
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class LocalStorageElement extends HTMLElement
|
|
26
|
+
{
|
|
27
|
+
// @attribute live - monitors localStorage change
|
|
28
|
+
// @attribute type - `text|json`, defaults to text, other types are compatible with INPUT field
|
|
29
|
+
constructor()
|
|
30
|
+
{
|
|
31
|
+
super();
|
|
32
|
+
const state = {}
|
|
33
|
+
, type = attr(this, 'type') || 'text'
|
|
34
|
+
, listener = e=> e.detail.key === attr( this,'key' ) && propagateSlice()
|
|
35
|
+
, propagateSlice = ()=>
|
|
36
|
+
{ for( let parent = this.parentElement; parent; parent = parent.parentElement)
|
|
37
|
+
if( parent.onSlice )
|
|
38
|
+
return parent.onSlice(
|
|
39
|
+
{ detail: string2value( type, localStorage.getItem( attr( this, 'key' ) ) )
|
|
40
|
+
, target: this
|
|
41
|
+
} );
|
|
42
|
+
console.error(`${this.localName} used outside of custom-element`)
|
|
43
|
+
debugger;
|
|
44
|
+
};
|
|
45
|
+
this.sliceInit = s =>
|
|
46
|
+
{ if( !state.listener && this.hasAttribute('live') )
|
|
47
|
+
{ state.listener = 1;
|
|
48
|
+
window.addEventListener( 'local-storage', listener );
|
|
49
|
+
ensureTrackLocalStorage();
|
|
50
|
+
}
|
|
51
|
+
propagateSlice();
|
|
52
|
+
return s || {}
|
|
53
|
+
}
|
|
54
|
+
this._destroy = ()=>
|
|
55
|
+
{
|
|
56
|
+
if( !state.listener )
|
|
57
|
+
return;
|
|
58
|
+
state.listener && window.removeEventListener('local-storage', listener );
|
|
59
|
+
delete state.listener;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
disconnectedCallback(){ this._destroy(); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
window.customElements.define( 'local-storage', LocalStorageElement );
|
|
66
|
+
export default LocalStorageElement;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epa-wg/custom-element",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"types": "./custom-element.d.ts",
|
|
17
17
|
"scripts": {
|
|
18
|
+
"dev:help": "echo \"needed for sandbox demo\"",
|
|
18
19
|
"dev": "bash bin/stackblitz.sh",
|
|
19
20
|
"start": "npm i --no-save @web/dev-server && web-dev-server --node-resolve",
|
|
20
21
|
"test": "echo \"test would reside in https://github.com/EPA-WG/custom-element-test\" && exit 0",
|
package/request.html
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
5
|
+
<title>custom-element Declarative Custom Element implementation demo</title>
|
|
6
|
+
<script type="module" src="custom-element.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
body,nav{ display: flex; flex-wrap: wrap; align-content: stretch; gap: 1rem; }
|
|
9
|
+
nav{ flex-direction: column;}
|
|
10
|
+
dce-link,dce-1-slot,dce-2-slot,dce-3-slot,dce-4-slot,dce-2-slots,greet-element,pokemon-tile
|
|
11
|
+
{ box-shadow: 0 0 0.5rem lime; padding: 1rem; display: inline-block;}
|
|
12
|
+
dd{ padding: 1rem;}
|
|
13
|
+
p{ margin: 0;}
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
|
|
18
|
+
<html-demo-element legend="1. simple payload"
|
|
19
|
+
description="payload is ignored as in DCE definition there is no default slot">
|
|
20
|
+
<template>
|
|
21
|
+
<custom-element tag="poke-image-request" hidden>
|
|
22
|
+
<img src="https://unpkg.com/pokeapi-sprites@2.0.2/sprites/pokemon/other/dream-world/{//poke-id}.svg"/>
|
|
23
|
+
</custom-element>
|
|
24
|
+
<poke-image-request poke-id="1"></poke-image-request>
|
|
25
|
+
|
|
26
|
+
<custom-element tag="poke-list-request" hidden>
|
|
27
|
+
<a href="https://pokeapi.co/api/v2/pokemon?offset={//offset}&limit=10">list json</a>
|
|
28
|
+
</custom-element>
|
|
29
|
+
|
|
30
|
+
<custom-element tag="poke-list-page" hidden>
|
|
31
|
+
<a href="https://pokeapi.co/api/v2/pokemon?offset={//offset}&limit=10">list json</a>
|
|
32
|
+
<http-request-json
|
|
33
|
+
target="poke-list"
|
|
34
|
+
src="https://pokeapi.co/api/v2/pokemon?offset={//offset}&limit=10"
|
|
35
|
+
></http-request-json>
|
|
36
|
+
<ol>
|
|
37
|
+
<xsl:for-each select="">
|
|
38
|
+
|
|
39
|
+
</xsl:for-each>
|
|
40
|
+
</ol>
|
|
41
|
+
</custom-element>
|
|
42
|
+
<poke-list-page>
|
|
43
|
+
<poke-list-request offset="0"></poke-list-request>
|
|
44
|
+
</poke-list-page>
|
|
45
|
+
|
|
46
|
+
</template>
|
|
47
|
+
</html-demo-element>
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
<script type="module" src="https://unpkg.com/html-demo-element@1.0/html-demo-element.js"></script>
|
|
51
|
+
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|