@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.
@@ -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,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager">
4
+ <output url="file://$PROJECT_DIR$/out" />
5
+ </component>
6
+ </project>
@@ -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
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
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.7/coverage/coverage.svg
145
- [coverage-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.7/coverage/lcov-report/index.html
146
- [storybook-url]: https://unpkg.com/@epa-wg/custom-element-test@0.0.7/storybook-static/index.html?path=/story/welcome--introduction
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
- , XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform';
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 create = ( tag, t = '' ) =>
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 xml2dom( xmlString )
9
+ function
10
+ xml2dom( xmlString )
14
11
  {
15
12
  return new DOMParser().parseFromString( XML_DECLARATION + xmlString, "application/xml" )
16
13
  }
17
14
 
18
- function bodyXml( dce )
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 slot2xsl( s )
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 injectData( root, sectionName, arr, cb )
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 assureSlot( e )
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 class CustomElement extends HTMLElement
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,'&#38;')+'"');
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.getElementsByTagName( 'slot' ) ].forEach( slot2xsl );
110
+ [ ...this.templateNode.querySelectorAll('slot') ].forEach( slot2xsl );
60
111
  const p = new XSLTProcessor();
61
112
  p.importStylesheet( this.xslt );
62
- const tag = this.getAttribute( '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 f = p.transformToFragment( x, document );
75
- this.innerHTML = '';
76
- [ ...f.childNodes ].forEach( e => this.appendChild( e ) );
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 xslt()
175
+ get xsltString()
83
176
  {
84
- return xml2dom(
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>
@@ -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
- 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;}
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">
@@ -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.7",
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>