Skip to content

Commit 050e3e6

Browse files
committed
1st cut. npm test passed
1 parent 04a695c commit 050e3e6

16 files changed

+468
-1
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.idea
2+
13
# Logs
24
logs
35
*.log

.npmignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.git

README.md

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,59 @@
1+
# fetch-element and slotted-element
2+
Slots without shadow DOM primarily would be used for remotely fetched data render.
3+
That is why the `slotted-element` is derived from `fetch-element`.
4+
5+
Size is 5k+2k source, served total 5K gzipped.
6+
17
# slotted-element
2-
fetch-element and slotted-element
8+
1. Exposes API to work with slots programmatically by adding and removing slots by slot name.
9+
10+
The slots concept is described in
11+
[using slots in MDN](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots#adding_flexibility_with_slots)
12+
13+
Originally it works in conjunction with shadowDOM when slots defined in content of element and referenced in
14+
rendered shadowDOM by name. I.e. rendered DOM defines which slot and where will be displayed inside of web component.
15+
16+
`slotted-element` gives ability to manipulate slots programmatically without engaging shadows dom.
17+
18+
## API
19+
### Attributes
20+
21+
# fetch-element
22+
23+
1. exposes [fetch() api](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) as web component.
24+
2. Input parameters and result are exposed via element attributes
25+
3. Provides default rendering for HTML and JSON as table
26+
4. Exposes overriding methods for fetch and render life cycle
27+
28+
## API
29+
### attributes
30+
all attributes reflected as component properties
31+
* `src` - url for data retrieval ( `data:` as url TBD )
32+
* `method` - http method
33+
* `headers` - JS expression to be evaluated as string key-value object
34+
* `state` - '' or one of `loading`( fetch started ), `rendering`, `loaded`, `error`
35+
* `status` - http code response.status
36+
37+
NOTE: for defining the payload in http request leave `src` undefined and call `fetch(url, options)` with needed parameters
38+
39+
### methods
40+
* `fetch( url, options )` - return Promise resolved upon rendering
41+
`abort()` - to interrupt fetch in progress.
42+
* `get headers()` - overrides headers key-value string pairs
43+
* `onResponse( response )` - response from `fetch()` call. Sets `status`, `contentType`, `responseHeaders`.
44+
Returns data promise from `response.json()` or ``response.text()`
45+
* `onResult( result )` - called when data available. Renders data as HTML or table( for json contentType ).
46+
Sets `state="loaded"`
47+
48+
Callbacks:
49+
* `data2Html( result, contentType, status, responseHeaders )` - override to define custom render of data into HTML string
50+
* `onError( error )`
51+
* `json2table( data )` - default render JSON object or array to table. Override for custom render. Return html string.
52+
* `getKeys( obj )` - override to limit or define the order of keys on json object to be rendered in table.
53+
54+
## rendering by CSS
55+
`fetch-element` could be self-sufficient without using a slots pattern: the `state` attribute is available to trigger
56+
visibility of internal dom subtree branches by `[state="xxx"] ...` selector.
57+
58+
# test
59+
reside in separate repository https://github.com/sashafirsov/slotted-element-test

demo/doc.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "Doc",
3+
"title": "Dwarf",
4+
"age": 50,
5+
"portrait": "doc.png"
6+
}

demo/doc.png

11.3 KB
Loading

demo/dwarfs.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[ { "name": "Doc" , "Age guess":50, "title": "The leader of the dwarfs, required to be pompous, self-important and bumbling" }
2+
, { "name": "Grumpy" , "Age guess":17, "title": " Doc and Grumpy arguing about whether Snow White should stay with them" }
3+
, { "name": "Happy" , "Age guess":17, "title": " :) " }
4+
, { "name": "Sleepy" , "Age guess":17, "title": " :-O " }
5+
, { "name": "Bashful" , "Age guess":17, "title": "reluctant to draw attention to oneself; shy." }
6+
, { "name": "Sneezy" , "Age guess":17, "title": "inclined to sneeze. ill, sick - affected by an impairment of normal physical or mental function" }
7+
, { "name": "Dopey" , "Age guess":17, "title": "stupefied by sleep or a drug." }
8+
]

demo/embedded.html

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
From <b>embedded.html</b> 😏

demo/index.html

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!doctype html>
2+
<html lang="en-GB">
3+
<head>
4+
<meta charset="utf-8">
5+
<style>
6+
body {
7+
background: #fafafa;
8+
}
9+
demo-section{ display: block; border: blueviolet dashed 1px; border-radius: 1rem; padding: 1rem; }
10+
</style>
11+
</head>
12+
<body>
13+
<script type="module" src="../slotted-element.js"></script>
14+
15+
<demo-section>
16+
<slotted-element src="embedded.html"></slotted-element>
17+
</demo-section>
18+
19+
<demo-section>
20+
<slotted-element src="https://badUrl.heck">
21+
<p slot="loading">Loading... ⏳ </p>
22+
<p slot="error">Something went wrong. 😭 </p>
23+
</slotted-element>
24+
</demo-section>
25+
<script src="../node_modules/prismjs/prism.js"></script>
26+
<script type="module">
27+
class DemoSection extends HTMLElement
28+
{
29+
// constructor(){ super(); }
30+
connectedCallback()
31+
{
32+
const html = Prism.highlight( this.innerHTML, Prism.languages.html, 'html' );
33+
const code = document.createElement( 'code' );
34+
code.className='language-markup';
35+
code.innerHTML = html+'<hr/>';
36+
this.insertBefore( code, this.firstChild );
37+
}
38+
};
39+
window.customElements.define( 'demo-section', DemoSection);
40+
// https://www.webcomponents.org/search/highlight
41+
// https://www.webcomponents.org/element/@kuscamara/code-sample
42+
// https://www.webcomponents.org/element/@vanillawc/wc-markdown
43+
</script>
44+
</body>
45+
</html>

demo/render-from-json.html

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>render from json - FetchElement</title>
6+
</head>
7+
<body>
8+
<fieldset>
9+
<legend>Custom renderer</legend>
10+
11+
<json-render-element src="doc.json"></json-render-element>
12+
<script type="module">
13+
import FetchElement from '../src/FetchElement.js';
14+
window.customElements.define('json-render-element',
15+
class JsonElement extends FetchElement
16+
{
17+
data2Html(json)
18+
{
19+
return `<h1>${ json.name }</h1>
20+
<img src="${ json.portrait }" />
21+
`;
22+
}
23+
});
24+
</script>
25+
</fieldset>
26+
27+
<fieldset>
28+
<legend>Default as table</legend>
29+
30+
<script type="module" src="../fetch-element.js"></script>
31+
<fetch-element src="doc.json"></fetch-element>
32+
</fieldset>
33+
34+
<fieldset>
35+
<legend>Array as table</legend>
36+
37+
<fetch-element src="dwarfs.json"></fetch-element>
38+
</fieldset>
39+
40+
</body>
41+
</html>

fetch-element.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { FetchElement } from './src/FetchElement.js';
2+
3+
window.customElements.define('fetch-element', FetchElement);

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SlottedElement } from './src/SlottedElement.js';

package.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "slotted-element",
3+
"version": "0.0.0",
4+
"description": "Webcomponent fetch-element and slotted-element",
5+
"author": "slotted-element",
6+
"license": "MIT",
7+
"main": "index.js",
8+
"module": "index.js",
9+
"type": "module",
10+
"scripts": {
11+
},
12+
"dependencies": {
13+
},
14+
"devDependencies": {
15+
}
16+
}

slotted-element.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { SlottedElement } from './src/SlottedElement.js';
2+
3+
window.customElements.define('slotted-element', SlottedElement);

src/FetchElement.js

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
export function
2+
wait4DomUpdated( cb )
3+
{ return new Promise( resolve =>
4+
{ const assureDom = window.requestIdleCallback || window.requestAnimationFrame
5+
, done = ()=> resolve(cb && cb() );
6+
assureDom ? assureDom( done ) : setTimeout( done, 100 );
7+
})
8+
}
9+
export function
10+
toKebbabCase(s){ return (s||'').toLowerCase().replaceAll(/\s/g ,'-')}
11+
export class
12+
FetchElement extends HTMLElement
13+
{
14+
static get observedAttributes(){ return [ 'src', 'method', 'headers', 'state', 'status', 'error' ]; }
15+
16+
get headers(){ return {} }
17+
abort(){}
18+
fetch(){}
19+
20+
constructor()
21+
{ super();
22+
let promise = Promise.resolve();
23+
const controller = new AbortController();
24+
const { signal } = controller;
25+
26+
this.abort = ()=> controller.abort();
27+
this.fetch = async ( url, options )=>
28+
{
29+
this.state = 'loading';
30+
this.status = '';
31+
32+
const opt = { method : this.getAttribute( 'method' ) || 'GET'
33+
, headers : this.headers
34+
, ...options
35+
, signal
36+
};
37+
38+
promise = new Promise(async (resolve, reject) =>
39+
{ try
40+
{ const response = await fetch( url, opt );
41+
const ret = await this.onResponse( response )
42+
const res = await this.onResult( ret );
43+
this.error ? reject( this.error ) : resolve( res );
44+
}catch( ex )
45+
{ reject( this.onError( ex ) ); }
46+
})
47+
};
48+
Object.defineProperty( this, 'promise', { get(){ return promise; } } );
49+
FetchElement.observedAttributes
50+
.filter( prop => prop !=='headers' )
51+
.forEach( prop => Object.defineProperty( this, prop,
52+
{ get : () => this.getAttribute( prop )
53+
, set: val => this.setAttribute( prop, val )
54+
} ));
55+
}
56+
57+
attributeChangedCallback( name, oldValue, newValue )
58+
{
59+
switch( name )
60+
{ case 'headers':
61+
this[ name ] = eval( newValue );
62+
break;
63+
case 'src':
64+
this.fetch( newValue );
65+
// setTimeout( () => this.fetch( newValue ),0 );
66+
break;
67+
default:
68+
if( this[ name ] !== newValue )
69+
this[ name ] = newValue;
70+
}
71+
}
72+
73+
async onResponse( response )
74+
{ const s = 1*( this.status = response.status);
75+
if( s<200 || s>=300 )
76+
this.error = 'network error';
77+
this.contentType = response.headers.get( 'content-type' );
78+
this.responseHeaders = response.headers;
79+
if( this.contentType.includes( 'json' ) )
80+
return response.json();
81+
return response.text();
82+
}
83+
84+
async onResult( result )
85+
{
86+
try
87+
{
88+
this.state = 'rendering';
89+
90+
if( this.contentType.includes( 'xml' ) )
91+
{
92+
const xml = ( new window.DOMParser() ).parseFromString( result, "text/xml" );
93+
this.data2Html( xml, this.contentType, this.status, this.responseHeaders );
94+
// todo xslt from xml
95+
}else if( this.contentType.includes( 'html' ) )
96+
{
97+
this.innerHTML = result;
98+
await wait4DomUpdated();
99+
this.data2Html( result, this.contentType, this.status, this.responseHeaders );
100+
await wait4DomUpdated();
101+
}else if( this.contentType.includes( 'json' ) )
102+
{ await wait4DomUpdated();
103+
const html = this.data2Html( result, this.contentType, this.status, this.responseHeaders );
104+
this.innerHTML = html || this.json2table(result);
105+
await wait4DomUpdated();
106+
}
107+
}finally
108+
{
109+
this.state = 'loaded';
110+
}
111+
}
112+
data2Html( data, contentType, code ){}
113+
onError( error ){ this.state = 'error'; return error; }
114+
115+
getKeys( obj ){ return Object.keys(obj); }
116+
117+
json2table( data )
118+
{
119+
if( Array.isArray(data) )
120+
{ if( !data.length )
121+
return ''
122+
const keys = this.getKeys( data[0] );
123+
124+
return `
125+
<table>
126+
<tr>${keys.map( k=>`<th>${k}</th>` ).join('\n')}</tr>
127+
${data.map( r=>`
128+
<tr>${ keys.map(k=>`
129+
<td key="${toKebbabCase(k)}">
130+
${this.json2table(r[k])}
131+
</td>`).join('')
132+
}
133+
</tr>`).join('\n')}
134+
</table>
135+
` }
136+
if( typeof data !== 'object' || data === null )
137+
return data;
138+
const keys = this.getKeys( data );
139+
return `
140+
<table>
141+
${ keys.map( k=>`
142+
<tr><th>${k}</th>
143+
<td key="${toKebbabCase(k)}">${ this.json2table(data[k]) }</td>
144+
</tr>` ).join('')}
145+
</table>
146+
`
147+
}
148+
149+
}
150+
export default FetchElement;

0 commit comments

Comments
 (0)