wc-template
v1.0.0
Published
generate custom element from html template
Maintainers
Readme
wc-template
Create custom elements using template and link elements.
Usage
The script should be added after template and link with wc-template,
and after style and link stylesheet with wc-global-css attribute:
<script src='https://cdn.jsdelivr.net/npm/wc-template@1/wc-template.js'></script>or copy it to your project:
<script src='path/to/wc-template.js'></script>Examples
Go to the examples. Or run it locally by cloning this repository and run:
npm install
npm testAPI
template
<!-- create custom element with name custom-element -->
<template wc-template='custom-element'>
</template>link
<!-- link to external template -->
<link rel='preload' as='fetch' crossorigin wc-template href='templates.html'/>observed-attributes
<!-- add observed attributes -->
<template wc-template='custom-element' observed-attributes='custom-attr-1 custom-attr-2 custom-attr-3'>
</template>shadow-root
<!-- add shadow root -->
<template wc-template='custom-element'>
<shadow-root>
<p>Hello World</p>
</shadow-root>
</template><!-- shadow root options
mode: default value is 'open', available values are 'open' and 'closed'
clonable: default value is false
delegates-focus: default value is false
serializable: default value is false
slot-assignment: default to 'named', available values are 'named' and 'manual'
-->
<template wc-template='custom-element'>
<shadow-root mode='closed' clonable delegates-focus serializable slot-assignment='manual'>
<p>Hello World</p>
</shadow-root>
</template>CSS
<!-- attach wc-global-css styles to shadow root -->
<link rel='stylesheet' href='style.css' wc-global-css/>
<style wc-global-css>
p { color: #f00; }
</style>
<template wc-template='custom-element'>
<shadow-root wc-global-css>
<p>Hello World</p>
</shadow-root>
</template><!-- attach display style to shadow root, it also adds :host([hidden]) { display: none; } -->
<template wc-template='custom-element'>
<shadow-root display='inline-block'>
<p>Hello World</p>
</shadow-root>
</template><!-- add styles -->
<template wc-template='custom-element'>
<shadow-root display='inline-block'>
<p>Hello World</p>
</shadow-root>
<style>
:host {
width: 300px;
}
p {
color: #0f0;
}
</style>
<!-- external css with same url will be optimized to only use one CSSStyleSheet instance across different wc-template -->
<link rel='stylesheet' href='style.css'/>
</template><!-- order matters -->
<style wc-global-css>
p {
color: red;
}
</style>
<!-- result: color: blue -->
<template wc-template='custom-element'>
<shadow-root display='block' wc-global-css>
<p>Hello World</p>
</shadow-root>
<style>
p {
color: blue;
}
</style>
</template><!-- order matters -->
<style wc-global-css>
p {
color: red;
}
</style>
<!-- result: color: red -->
<template wc-template='custom-element'>
<style>
p {
color: blue;
}
</style>
<shadow-root display='block' wc-global-css>
<p>Hello World</p>
</shadow-root>
</template>script
<template wc-template='custom-element' observed-attributes='customer'>
<shadow-root display='block' wc-global-css mode='closed'>
<p><span id='hello'></span>, <span id='name'></span></p>
<p id='html'></p>
<p id='htmlUnsafeString'></p>
</shadow-root>
<style>
#hello { color: #f00; }
#name { color: #7a3; }
</style>
<!-- script run on constructor -->
<script>
// available variables:
// shadowRoot, id, $, html, htmlUnsafeString, baseURL, State, Static, PRIVATE
// shadowRoot is available even when mode is closed
for (const style of shadowRoot.adoptedStyleSheets)
console.log(style);
// use id to access elements
id.hello.textContent = 'Hello World';
// use $ to access State instance, attributes are automatically included
// note that attributes are null at constructor
$.LISTEN('customer', () => id.name.textContent = $.customer);
// $.STARTED is false at constructor and become true when first connectedCallback is called
console.log($.STARTED);
// $.CONNECTED is false at constructor, become true when connectedCallback is called
// and become false when disconnectedCallback is called
console.log($.CONNECTED);
// call INIT to run listener immediately
$.LISTEN('STARTED', () => console.log('STARTED', $.STARTED)).INIT();
$.LISTEN('CONNECTED', () => console.log('CONNECTED', $.CONNECTED)).INIT();
// html literal
// use baseURL to resolve relative path
id.html.innerHTML = html`<img src='${String(new URL('image.png', baseURL))}' alt='img'/>`;
// htmlUnsafeString: string is not escaped
id.htmlUnsafeString.innerHTML = htmlUnsafeString('<span style="color: blue;">Hello World</span>');
// State constructor
console.log('$ instance of State', $ instanceof State); // true
// Static constructor
console.log('this instance of Static', this instanceof Static); // true
console.log('this.constructor === Static', this.constructor === Static); // true
// PRIVATE symbol to hold private data on this and Static
this[PRIVATE].data = 10;
// create state
$.INIT('state0', 4);
console.log('state0', $.state0);
// listen to state change, state is changed when old value and new value differ according Object.is
const listener0 = $.LISTEN('state0', () => console.log('state0', $.state0));
$.state0 = 4; // listeners won't be called
$.state0 = 5; // listeners will be called
// unlisten
listener0.UNLISTEN();
$.state0 = 6;
// create state with force, listeners will be called even when old value and new value are same
$.INIT('state1', 1, 'force');
const listener1 = $.LISTEN('state1', () => console.log('state1', $.state1)).INIT();
$.state1 = 1;
// listen to multiple states
$.LISTEN('state0', 'state1', () => console.log('state0', $.state0, 'state1', $.state1));
// listener called twice
$.state0 = 9;
$.state1 = 10;
// batch multiple write, listener only called once
$.BATCH();
$.state0 = 11;
$.state1 = 12;
$.COMMIT();
// attach state as property
$.INIT('state2', 5).ATTACH(this);
console.log(this.state2);
// with different name
$.INIT('state3', 7).ATTACH(this, '_state3');
console.log(this._state3);
// read only
$.INIT('state4', 9).ATTACH(this, null, 'ro');
try { this.state4 = 10; } catch(e) { console.error(e); }
// attach to a property that already exists
this.state5 = 19;
$.INIT('state5', 0).ATTACH(this);
console.log('state5', $.state5); // 19
</script>
<!-- static script, called and awaited before registering custom element -->
<script static>
// available variables
// Static, PRIVATE, State, baseURL, html, htmlUnsafeString, styles, getGlobalCSS, updateGlobalCSS
// create shared State instance
const $ = Static[PRIVATE].state = new State();
$.INIT('shared0', 'home');
// await styles
await styles.whenLoaded();
console.log('wc-global-css', styles[0] === getGlobalCSS());
// control wc-global-css
document.head.insertAdjacentHTML('beforeend', html`
<style wc-global-css>
p { font-style: italic; }
</style>`);
updateGlobalCSS();
// dynamically create custom element
const template = document.createElement('template');
template.setAttribute('wc-template', 'my-element');
template.innerHTML = html`
<shadow-root><p>Hello World</p></shadow-root>
<script static>
alert('my-element');
</${'script'}>`;
document.head.appendChild(template);
</script>
<!-- async script run on constructor -->
<script async>
// OK
$.INIT('async0');
await Promise.resolve();
// throw Error, INIT can't be called after constructor return
$.INIT('async1');
</script>
</template>