Skip to content

mfcc64/wc-template

Repository files navigation

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 test

API

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>

About

Create custom elements using template and link elements

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors