skinny-widgets

skinnable web components widgets collection

Usage no npm install needed!

<script type="module">
  import skinnyWidgets from 'https://cdn.skypack.dev/skinny-widgets';
</script>

README

Skinny Widgets

Skinnable component set based on W3C standards compilant technologies.

Allows you to develop ui abstract code and easily switch between layouts without rewriting the code.

If you are looking for web components based data grid library check out gridy-grid project.

Note: starting from 0.2.0 Skinny Widgets is modular with divding to npm packages so you can find one-theme-only bundles for themes:

jquery sk-jquery-bundle antd sk-antd-bundle default sk-default-bundle

or go completely without bundles installing only elements you use. Like with the following command:

npm install sk-input sk-input-jquery

Here is the list of bundled elements:

sk-input sk-button sk-select sk-switch sk-checkbox sk-datepicker sk-alert sk-dialog sk-tabs sk-accordion sk-navbar sk-menu sk-app sk-tree sk-form sk-spinner

Examples

To see examples run http server to serve files

npm start

open one of examples in browser, e.g. http://127.0.0.1:8080/examples/compat/browser-compatible.html

For more examples check skinny-widgets-samples project.

Configuration

All-Inclusive Elements autoloading

If you don't need to do advanced configurations you can enable sk elements autoloading by adding sk-auto and sk-theme-{theme} html or body element class. Elements autoregister will run at the end of skinny-widgets-bundle.js loading so to start using skinny-widgets components you will have to just add one script and one attribute to your document.

<html class="sk-auto sk-theme-jquery">
<script src="/node_modules/skinny-widgets/dist/sk-compat-bundle.js"></script>
<sk-button theme="jquery">Hello</sk-button>
</html>

Template preload can be disabled by adding class sk-auto-notpl.

Modular loading

Skinny Widgets are developed with ESM and WHATWG modules in mind, so you can just import element classes and define them as Custom Elements.

<sk-config 
    id="configEl"    
    theme="jquery"
    base-path="/node_modules/skinny-widgets/src"
></sk-config>
<sk-button id="myButton" button-type="primary">
    MyButton
</sk-button>
<script type="module">
    import { SkConfig } from '/node_modules/sk-core/src/sk-config.js';
    import { SkButton } from '/node_modules/sk-button/src/sk-button.js';
    customElements.define('sk-config', SkConfig);
    customElements.define('sk-button', SkButton);
    myButton.addEventListener('click', (event) => {  
        alert("I'm clicked !");
    });
</script>

Some of them may rely on 3rd-party libraries and can be plugged at runtime or sometimes not.

As browsers still doesn't support relative dependency paths and importmaps you may need some code to resolve dependency on dateutils and other possible libraries.

<script defer src="/node_modules/es-module-shims/dist/es-module-shims.js"></script>
<script type="importmap-shim">
{
  "imports": {
    "dateutils": "/node_modules/dateutils/src/global.js",
    "dateutils/": "/node_modules/dateutils/"
  }
}
</script>
<script type="module-shim">
    import { SkDatePicker } from '/node_modules/sk-datepicker/src/sk-datepicker.js';
    customElements.define('sk-datepicker', SkDatePicker);
</script>
<sk-datepicker theme="antd" id="myDatePicker" value="12/25/2019"></sk-datepicker>

Although you're still free to use static scripts with bundle that exposes skinny web components into global scope with transpilation for older browsers. You may still need to load polyfills before bundle. Check examples for more info.

<script src="/dist/sk-compat-bundle.js"></script>
<script type="module">
    customElements.define('sk-config', SkConfig);
    customElements.define('sk-button', SkButton);
</script>

sk-config element is acts as common configuration host service, elements tries to find it on connectCallback and get necessary configurations, so you don't have to repeat params for every element. If config-sl attribute is not specified config element it search by configEl window property, then document is queried by sk-config tag name.

<sk-config 
    styles='{"default.css": "/node_modules/sk-theme-default/default.css"}'
    theme="jquery"
    base-path="/node_modules/skinny-widgets/src"
></sk-config>

You can override config search selector for every element by defining config-sl attribute:

<sk-config 
    id="alternativeConfigSelector"
    theme="jquery"
    base-path="/node_modules/skinny-widgets/src"
></sk-config>
<sk-input config-sl="#alternativeConfigSelector"></sk-input>

Attributes

theme - one of supported themes: e.g. 'antd' or 'jquery' (default: 'default')

base-path - path to sources and resources root, (default: '/node_modules/skinny-widgets/src/')

tpl-path - override url for template loading (only individual)

theme-path - url prefix lo toad resources (css, styles) from, starting from 0.2.0, defaults to ${basePath}/theme/${themeName} if not set or overriden by element impl

styles - set of style definitions used by elements as Object { 'name': 'path' }. Commonly styles are mounted into shadow root by given path. This allows to provide CSS isolation features saving performance with file browser cashing. Theme code has style file defaults. This files are that are needed to be applied to every component. With theme-path you can override the path to all them at once. Style attribute allows to completely change url for this files to load. This doesn't affect files attached by link tags in templates except they have higher override priority. When set to "false" disables theme styles automount into element's shadow root (default: undefined)

use-shadow-root - specifies if Shadow Root encapsulation is enabled for elements (default: 'true')

lang - locale code, currently used only for datepicker (default: 'en_US')

i18n - object with translation values that can be used with this.tr() method, e.g.

this.tr('Hello, {{ username }}', { username: 'John'});

to render this, element must have attribute set at i18n='{"Hello, {{ username }}": "Hallo, {{ username }}"}'. If it doesn't, original rendered string is returned.

Placeholders also can be passed with {0} format, in this case values will be taken from the rest of method arguments.

This values can be also set via element's instance property with the same name.

reflective - element auto re-render on external events, currently sk-config attrs change (default: true)

gen-ids - generate path based ids if absent (default: 'false')

rd-var-render - render option that switches template variable rendering modes between native template with simplified handlebars like support, original handlebars library usage (from window.Handlebars), same named external function call binded to renderer (you can assign own renderer class with method render(tpl, data) to it's _templateEngine property) search in window global or no variable rendering (default: 'true')

rd-cache - defines whether template cache is enabled or not (default: 'true')

rd-cache-global - allows to store and reuse templates in document body (default: 'true')

rd-tpl-fmt - defines template tag format, 'hadlebars' can be specified to generate templates with script tag instead of native template tag (default: undefined)

rd-own - creates own instance of renderer

tpl-vars - template prerendering variables override, e.g. tpl-vars='{"themePath": "../"}' will override themePath for template rendering context

jsonpath-dri-cn - use alternative jsonpath driver class e.g. "JsonPathPlusDriver"

jsonpath-dri-path use alternative jsonpath driver path e.g. "/node_modules/sk-core/src/impl/jsonpath/jsonpath-plus-driver.js"

st-store - store element's state to external storage otherwise if needed it's placed in element's attribute 'state', supported values: 'session', (default: undefined)

For session store elements are identified by cid that is set from specified id or generated (from dom path by default), so you will have to avoid id duplicates for this case.

log-lv - enabled log levels, possible values are "info", "debug", "warn", "error". Levels do not include each other, so enabling debug will not take every other label but only allow messages logged with this level (default: "info,error,warn")

log-t - log target can be "memory" to agregate messages in logger's property or "console" to use browser's console, you also can define your own overriding SkElement.createLogger() method, (default: not set === "console")

attr-plugins enable plugin attributes scan, additional definitions like: plg-${name}-cn="MyPlugin" plg-${name}-path="/my-plugin.js" will enable automatic plugin loading and binding

Configure with Json Object

You can specify json object with the same camel-cased config options. If element is present on page this object will be source of absent options, if not internal element will be created for every element. It is more performance friendly to have in document even when it is empty.

<script>
    window.skConfig = {
        theme: 'jquery'
    };
</script>
<sk-config></sk-config>

You can control element creation policy to body/html tag class flags

<body class="sk-config-el-individual"></body>

possible values are:

sk-config-el-global - one global sk-config element will be created if absent and shared between elements

sk-config-el-individual - one global sk-config element will be created if absent and shared between elements

sk-config-el-disabled - sk-config elements will not be appended to document, although it will be present as this.configEl element's class property

Skins

antd - and.design framework styles are used for layout

jquery - web components styled for jquery-ui

default - web components and standard native browser elements and technologies are used for layout

Templates preload

http/2+ allows to do request multiplexing that means a lot of smaller requests to the same host are better than big blocking requests to multiple hosts. But if you think you still need to load all in one bundle, you can use aggregated template bundles loaded with js or included to page by server. You can rely on sk-auto template preloading or do manual preload.

<script>
    const loadTemplates = async () => {
        const response = await fetch('node_modules/skinny-widgets/dist/antd.templates.html')
            .then(response => response.text())
            .then(text => document.body.insertAdjacentHTML('beforeend', text));
    };
    loadTemplates();
</script>

Widgets

sk-button

<sk-button id="myButton" button-type="primary">
    MyButton
</sk-button>

slots

default (not specified) - contents inside button

label - the same as label

template

id: SkButtonTpl

sk-input

<sk-input id="myInput1" value="foobar"></sk-input>

attributes

value - value syncronized with internal native element

disabled - disabled attribute syncronized with internal native element

list - datalist attribute for input

slots

default (not specified) - draws label for input

label - draws label for input

<sk-input id="myInput1">
    <span slot="label">Some Label</span>
</sk-input>

template

id: SkInputTpl

sk-checkbox

slots

default (not specified) - draws label for input

label - draws label for input

attributes

value - value syncronized with internal native element

disabled - disabled attribute syncronized with internal native element

<sk-checkbox theme="antd" base-path="/node_modules/skinny-widgets/src" id="myCheckbox"></sk-checkbox>

template

id: SkCheckboxTpl

sk-datepicker

<sk-datepicker base-path="/node_modules/skinny-widgets/src" id="myDatePicker" value="12/25/2019"></sk-datepicker>

slots

default (not specified) - draws label for input

label - draws label for input

attributes

open - present if datepicker calendar widget is currently opened (for native datepicker only represents focus state)

fmt - date value format (default: 'm/d/Y')

  • {Date} d [01-31]
  • {Short_Day_Name} D [Su, Mo, Tu, We, Th, Fr, Sa]
  • {Date} j [1-31]
  • {Full_day_name} l [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
  • {Week_day_number} w 0=Sunday, 1=Monday, 2=Tuesday etc...
  • {Nth_day_of_year} z [1-365] except leap years
  • {Full_month_name} F [January, February, ...]
  • {Month_number} m [01-12]
  • {Month_name_stripped_to_three_letters} M [Jan, Feb, ...]
  • {Month_number} n [1-12]
  • {Days_in_current_month} t [28-31]
  • {Full_year} Y [1900, ...]
  • {Last_two_digits_of_a_year} y [01-99]
  • {Time_postfix} a [am|pm]
  • {Time_postfix} A [AM|PM]
  • {Hours_in_12h_format} g [1-12]
  • {Hours_in_24h_format} G [0-23]
  • {Hour_in_12h_format_with_padding} h [01-12]
  • {Hours_in_24h_format_with_padding} H [00-23]
  • {Minutes_with_padding} i [00-59]
  • {Seconds_with_padding} s [00-59]
  • {Timezone} Z 2 for GMT+2

template

id: SkDatePickerTpl

sk-alert

<sk-alert type="error">Error ! Error !</sk-alert>

slots

default (not specified) - alert contents

attributes

closable - close button

template

id: SkAlertTpl

sk-dialog

<sk-button theme="antd" base-path="/node_modules/skinny-widgets/src" id="myButton" button-type="primary">
    <span slot="label">Show Dialog</span>
</sk-button>
<sk-dialog theme="antd" base-path="/node_modules/skinny-widgets/src" id="myDialog" title="Some Title" type="confirm">
    Hello Dialog !
</sk-dialog>
<script type="module">
    import { SkButton } from '/node_modules/sk-button/src/sk-button.js';
    import { SkDialog } from '/node_modules/sk-dialog/src/sk-dialog.js';
    customElements.define('sk-button', SkButton);
    customElements.define('sk-dialog', SkDialog);

    myButton.addEventListener('click', (event) => {
        myDialog.open();
    });
    myDialog.onconfirm = function(event) {
        console.log('confirmed');
        this.close();
    };
    myDialog.oncancel = function(event) {
        console.log('cancelled');
        this.close();
    };
</script>

As most of sk-elements sk-dialog by default renders it's contents inside Shadow DOM. So in case you want access some its data you will have to to extend SkDialog class, implement getters that will access internals and return values and register class as new Custom Element. It is quite easy and gives a better solution for code structure;)

slots

label - dialog window title

attributes

to-body - sometimes browser says it doesn't support dialogs in shadow dom (certainly with polyfill), but you can force it remount to body, although beaware, styles will be remounted too. Remounting will flush all event bindings added with .addEventListener() on internals so you will have to bind, everything at least on dialog instance or with dynamic late binding in addition components must be able to work property with rerendering, e.g. dump and restore state to externals or attributes.

open - identifies dialog state, you can control it (open or close) by adding or removing this attribute

dialog-tn - dialog element tag name (default: 'dialog'), set to 'div' in case you want to have no-dialog element dialog

Extending dialog actions

Sk Dialog allows you to use your own action buttons that are binded to methods and events by action (for sk-button) or data-action (for button) attributes. You can provided needed set of buttons by overriding dialog footer template.

<sk-dialog id="myDialog" title="Some Title" type="confirm" to-body>
    myDialog
    <sk-input id="skInput" sk-required></sk-input>
    <sk-checkbox id="skCheckbox" sk-required></sk-checkbox>
    <sk-switch id="skSwitch" sk-required></sk-switch>
    <template id="SkDialogFooterTpl">
        <div>
            <sk-button action="cancel" type="button">Cancel</sk-button>
            <sk-button action="save" type="button" button-type="primary">Save</sk-button>
        </div>
    </template>

</sk-dialog>
<script type="module">
    import { SkDialog } from '/node-modules/sk-dialog/src/sk-dialog.js';
    customElements.define('sk-dialog', SkDialog);
    myDialog.addEventListener('confirm', function(event) {
        console.log('confirmed');
    });
    myDialog.addEventListener('yo', function(event) {
        console.log('yoyo');
    });
    myDialog.oncancel = function (event) {
        console.log('cancelled');
        this.close();
    };
</script>

Note: Template ids must be prefixed according to constructor/class name, e.g. in case you are doing dialog with, extending SkDialog class your footer template fill be search by id "YourClassNameFooterTpl".

template

id: SkDialogTpl, SkDialogFooterTpl

sk-tabs

<sk-config
    theme="antd"
    base-path="/node_modules/skinny-widgets/src"
    lang="ru"
></sk-config>
<sk-tabs id="tabs">
    <sk-tab title="foo">
        some foo tab contents
    </sk-tab>
    <sk-tab title="bar">
        some bar tab contents
    </sk-tab>
    <sk-tab title="baz">
        some baz tab contents
    </sk-tab>
</sk-tabs>

attributes

disabled - all tabs disabled

template

id: SkTabTpl

sk-accordion

<sk-config
    theme="antd"
    base-path="/node_modules/skinny-widgets/src"
></sk-config>
<sk-accordion id="accordion">
    <sk-tab title="foo">
        some foo tab contents
    </sk-tab>
    <sk-tab title="bar">
        some bar tab contents
    </sk-tab>
    <sk-tab title="baz">
        some baz tab contents
    </sk-tab>
</sk-accordion>

attributes

disabled - all tabs disabled

mono - open only one tab at once

template

id: SkAccordionTpl

sk-navbar

<sk-config
    theme="antd"
    base-path="/node_modules/skinny-widgets/src"
></sk-config>
<sk-navbar>
    Nabar Contents
</sk-navbar>

you can enable panel and specify template for contents as path or using caching mechanics

<sk-navbar id="navbar1" align="top" panel panel-tpl-path="fakepath">
    Some top Contents
    Bar
    <template id="SkNavbarPanelTpl">
        <a href="/foo">foo</a>
        <a href="/bar">bar</a>
    </template>
</sk-navbar>

attributes

open - identifies and controls if navbar is open

align - can be "right", "left", "buttom" and "top" to define where navbar is nested (default: "left")

auto-open - enables auto open and close on mouse move close to navbar side

panel - show panel

panel-tpl-path - specify custom tpl path for panel (e.g. panel-tpl-path="/panel.tpl.html"), if not specified nothing is rendered

sticky - by default panels are sticky, but you can disable this by setting it to "false"

clength - contents pane width or height

template

id: SkNavbarTpl

sk-select

<sk-config
    styles='{"antd.css": "/node_modules/sk-theme-antd/antd.min.css"}'
    theme="antd"
    base-path="/node_modules/skinny-widgets/src"
    lang="ru"
    id="skConfig"
></sk-config>
<sk-select id="skSelect">
    <option value="default">default</option>
    <option value="antd">antd</option>
    <option value="jquery">jquery</option>
</sk-select>
<script type="module">
    import { SkConfig } from '/node_modules/sk-core/src/sk-config.js';
    import { SkSelect } from '/node_modules/sk-select/src/sk-select.js';

    customElements.define('sk-config', SkConfig);
    customElements.define('sk-select', SkSelect);

    skSelect.value = skConfig.getAttribute('theme');
    skSelect.addEventListener('change', (event) => {
        skConfig.setAttribute('theme', event.target.value);
    }, false);
</script>

attributes

multiple - sk-select draws multiple selection widget and represent selected options as comma-separated list.

value-type - for multiselect native - show only first selected value as native elements, selectOptions prop will give you all of them, default -- comma-separated list of values.

slots

label - draws label for select

template

id: SkSelectTpl

sk-switch

<sk-config
    styles='{"antd.css": "/node_modules/sk-theme-antd/antd.min.css"}'
    theme="antd"
    base-path="/node_modules/skinny-widgets/src"
></sk-config>
<sk-switch id="mySwitch" checked value="11"></sk-switch>
<script type="module">
    import { SkConfig } from '/node_modules/sk-core/src/sk-config.js';
    import { SkSwitch } from '/node_modules/sk-switch/src/sk-switch.js';

    customElements.define('sk-config', SkConfig);
    customElements.define('sk-switch', SkSwitch);
</script>

slots

default (not specified) - draws label for input

label - draws label for input

<sk-switch id="mySwitch1">Some Label</sk-switch>

template

id: SkSwitchTpl

sk-tree

<sk-tree link-tpl-str='<a href="?categoryId={{ id }}">{{ name }}</a>' tree-data='[{"id": 1, "name": "category1", "parentId": 0}, {"id": 2, "name": "category2", "parentId": 0}, {"id": 3, "name": "category11", "parentId": 1}, {"id": 4, "name": "category111", "parentId": 3}]'></sk-tree>

attributes

expanded - "false" value collapses the tree and renders it as collapsible, "true" renders as expanded but collapsible

selected-id - id of the element that's branch will be forced to be extended

link-tpl-str - inline template for each item contents

item-tpl-path - external template path for item

root-tn - root tag name (default: 'ul')

item-tn - item tag name (default: 'li')

root-cn - class name for root (default: 'sk-tree-root')

item-cn - class name for item (default: 'sk-tree-item')

branch-cn - class name for branch (default: 'sk-tree-has-child')

selectable - enable item by click selection forwarded to value attribute

template

id: SkTreeTpl, SkTreeItemTpl

sk-spinner

<sk-config
    styles='{"antd.css": "/node_modules/sk-theme-antd/antd.min.css"}'
    theme="antd"
    base-path="/node_modules/skinny-widgets/src"
    lang="ru"
></sk-config>
<sk-button id="skButton" button-type="primary">
    <span slot="label">Show Dialog</span>
</sk-button>
<sk-spinner id="skSpinner"></sk-spinner>

<script type="module">
    import { SkSpinner } from '/node_modules/sk-spinner/src/sk-spinner.js';
    import { SkButton } from '/node_modules/sk-button/src/sk-button.js';

    customElements.define('sk-spinner', SkSpinner);
    customElements.define('sk-button', SkButton);

    skButton.addEventListener('click', (event) => {
        skSpinner.dispatchEvent(new CustomEvent('toggle'));
    });
</script>

template

id: SkSpinnerTpl

sk-form

sk-form sends it's fields as FormData (multipart request), attributes are the same as for form element type

to have data submission working sk-form must have sk-button with type="submit" inside

supported form element types:

sk-input, sk-checkbox, sk-select, sk-datepicker, input, textarea

if novalidate attribute is not set validation is performed on submit

if validation is not passed forminvalid event is emitted with info in event.detail.errors

if validation successfull formvalid event is emitted and form attempts to send data

it throws formsubmitsuccess event on 200 or any success response

and formsubmiterror

request/response data is stored in event.detail.request callback argument field

<sk-form action="/submit" method="POST" id="myForm">
    <span slot="fields">
        <sk-input name="firstName" id="formInput1">
            <span slot="label">First Name</span>
        </sk-input>
        <sk-input name="secondName" id="formInput2">
            <span slot="label">Second Name</span>
        </sk-input>
        <sk-input name="address" id="formInput3">
            <span slot="label">Address</span>
        </sk-input>
        <sk-checkbox name="isSingle" id="formCheckbox"><span slot="label">Single</span></sk-checkbox>
        <sk-select name="gender" id="formSelect">
            <option value="male">Male</option>
            <option value="female">Female</option>
        </sk-select>
        <sk-button id="formButton" type="submit" button-type="primary">
            <span slot="label">Submit</span>
        </sk-button>
    </span>
</sk-form>


<script type="module">
    import { SkButton } from '/node_modules/sk-button/src/sk-button.js';
    import { SkInput } from '/node_modules/sk-input/src/sk-input.js';
    import { SkSelect } from '/node_modules/sk-select/src/sk-select.js';
    import { SkForm } from '/node_modules/sk-form/src/sk-form.js';
    import { SkCheckbox } from '/node_modules/sk-checkbox/src/sk-checkbox.js';

    customElements.define('sk-button', SkButton);
    customElements.define('sk-input', SkInput);
    customElements.define('sk-select', SkSelect);
    customElements.define('sk-form', SkForm);
    customElements.define('sk-checkbox', SkCheckbox);

    myForm.addEventListener('formsubmitsuccess', (event) => {
        console.log('form submit success handled', event);
    });
    myForm.addEventListener('formsubmiterror', (event) => {
        console.log('form submit error handled', event);
    });
</script>

attributes

action, method - same as for the form element

enctype - same as for form element, with additional application/json type support

response-validation - response validation integration method, set as "fasle" to disable

Action buttons

while bootstrap process sk-form lookups for all internal button and sk-button elements with and bind them with callback by name or as firing events (see similar sk-dialog functionality for examples). So you can extend form with your own button triggered logic.

Field Validation

You can bind custom validation function for every field:

<script>
    function validateFirstName(el) {
        if (el.value == '1') {
            return true;
        } else {
            return 'Wrong value';
        }
    }
</script>
<sk-input name="firstName" id="formInput1" form-validation="validateFirstName" form-validation-msg="{'someError': 'This field has some error'}">First Name</sk-input>

form-validation attribute specifies validation function name, it can be binded to global (window) or field class. Validation function returns error message or validation key to load from form-validation-msg or true in case value is valid.

To enable validation labels display use validation-label attribute of an element or sk-form, use value "disabled" to force it's switch off.

no-live-validation attribute disables field revalidation on input

Backend validation

You can use response-validation sk-form attribute or define 'validateResponse' method to provide post-submit validator for cases when backend can return validation result json. Xml Http Request Event will be passed as argument of this method and formsubmitsuccess or formsubmiterror events will be triggered depending on validator function execution result. As it is called with form context you can use form methods or resources to indicate error.

<sk-config
    theme="jquery"
    base-path="./node_modules/skinny-widgets/src"
    lang="ru"
    id="myConfig"
></sk-config>


<script type="module">
    import { SkCheckbox } from './node_modules/sk-checkbox/src/sk-checkbox.js';
    import { SkButton } from './node_modules/sk-button/src/sk-button.js';
    import { SkConfig } from './node_modules/sk-core/src/sk-config.js';
    import { SkSelect } from './node_modules/sk-select/src/sk-select.js';
    import { SkInput } from './node_modules/sk-input/src/sk-input.js';
    import { SkForm } from './node_modules/sk-form/src/sk-form.js';

    customElements.define('sk-config', SkConfig);
    customElements.define('sk-checkbox', SkCheckbox);
    customElements.define('sk-button', SkButton);
    customElements.define('sk-select', SkSelect);
    customElements.define('sk-input', SkInput);
    customElements.define('sk-form', SkForm);


    myForm.addEventListener('formsubmitsuccess', (event) => {
        console.log('form submit success handled', event);
    });
    myForm.addEventListener('formsubmiterror', (event) => {
        console.log('form submit success handled', event);
    });
    myForm.responseValidator = function(event) {
        let errors = event.response.fieldErrors;
        if (errors) {
            for (let fieldName of Object.keys(errors)) {
                let field = this.querySelector(`[name='${fieldName}']`);
                if (field) {
                    let message = '';
                    if (Array.isArray(errors[fieldName])) {
                        message = errors[fieldName].join(' ');
                    } else {
                        message = errors[fieldName];
                    }
                    field.setCustomValidity(message);
                    this.impl.renderFieldInvalid(field);
                }
            }
        }
        // remove error indication from valid fields
        let fields = this.impl.queryFields();
        for (let field of fields) {
            let fieldName = field.getAttribute('name');
            if (fieldName && ! errors[fieldName]) {
                this.impl.renderFieldValid(field);
            }
        }
    };
</script>

<sk-form id="myForm" action="/submit" method="POST" response-validation="responseValidator" validation-label>

        <sk-select id="skSelect2" name="skSelect2" multiple>
            <option value="apple">Apple</option>
            <option value="banana">Banana</option>
            <option value="lemon">Lemon</option>
        </sk-select>
        <sk-checkbox id="myCheckbox" name="myCheckbox">Checkbox</sk-checkbox>
        <sk-input id="myInput" name="myInput">My Input</sk-input>

    <sk-button id="formButton" type="submit" button-type="primary">Submit</sk-button>
</sk-form>

where response is like:

{
  "fieldErrors": {
    "myInput": [
      "Hey, your input is invalid !"
    ]
  },
  "result": "error"
}

template

id: SkFormTpl

Sk Validators

Sk Widgets has a set of validators you can combine with native and your custom validators. At least they have no 'initially invalid' behaviour. Validators are added to elements as attributes just as Validation API validators, but they have sk- prefix in names.

sk-required - checks that field is required.

sk-min - checks that field not less than value. The number of symbols is checked in case of string.

sk-max - checks that field is greater than value. The number of symbols is checked in case of string.

sk-email - checks that field is matches email validation regex.

sk-pattern - checks that field is matches provided validation regex.

<sk-input name="firstName" id="formInput1" sk-pattern="^[A-Za-z]+quot;>First Name</sk-input>

Validators can be used either inside sk-form or without it. For the second case 'input' event listeners are usually used otherwise validation is fully delegated for sk-form code and normally triggered by 'formsubmit' event.

Note: All behaviour is forwarded to Validation API Custom Error and Custom Validation Message mechanics.

sk-menu

sk-menu allows to specify menu or context menu in ui-independent way

<sk-config
    theme="jquery"
    base-path="/node_modules/sk-core/src"
    theme-path="/node_modules/sk-theme-jquery"
    lang="ru"
    id="skConfig"
></sk-config>
<sk-menu id="skMenu">
    <sk-menu-item>foo</sk-menu-item>
    <sk-menu-item>bar</sk-menu-item>
</sk-menu>
<script type="module">
    import { SkConfig } from '/node_modules/sk-config/src/sk-config.js';
    import { SkMenu } from '/node_modules/sk-menu/src/sk-menu.js';

    customElements.define('sk-config', SkConfig);
    customElements.define('sk-menu', SkMenu);
</script>

attributes

events

itemclick - triggered when menu item is clicked, detail: item - dom obj of item is clicked, origEvent - mouse click event

template

id: SkMenuTpl

sk-app

sk-app is route-to render component that can help you feel like with popular framework SPA app. It allows to map one or more areas rendering to router path changes. It uses navigo library as runtime dependency to you have to have it loaded before sk-widgets.

Note: SkApp page load is implemented with dynamic imports browser feature in old browsers it will be switched to script tag append load. If route value not specified as string (custom element tag name) component load is performed on route change.

attributes

dimport - "false" switches page load from dynamic import to tag appending, also used ass fallback if dynamic imports supported was not detected in browser. Use it when loading classes from bundles.

ro-root - root attribute for Navigo, default: "/"

ro-use-hash - use-hash attribute for Navigo, default: false

events

skroutestart - triggers on route is started

skrouteend - triggers on route is done

<script src="/node_modules/navigo/lib/navigo.min.js"></script>
<script>
    class PageAConfA extends HTMLElement {
        connectedCallback() {
            this.insertAdjacentHTML('beforeend', '<div>PageAConfA</div>');
        }
    }
    class PageAConfB extends HTMLElement {
        connectedCallback() {
            this.insertAdjacentHTML('beforeend', '<div>PageAConfB</div>');
        }
    }
    class PageBConfA extends HTMLElement {
        connectedCallback() {
            this.insertAdjacentHTML('beforeend', '<div>PageBConfA</div>');
        }
    }
    customElements.define('page-a-confa', PageAConfA);
    customElements.define('page-a-confb', PageAConfB);
    customElements.define('page-b-confa', PageBConfA);
</script>
<script src="/dist/sk-compat-bundle.js"></script>

<a href="page-a" data-navigo>Page A</a>
<a href="page-b" data-navigo>Page B</a>

<sk-config
        theme="jquery"
        base-path="/src"
        lang="en"
        id="configA"
        routes='{"page-a": "page-a-confa", "page-b": "page-b-confa"}'
></sk-config>
<sk-app configSl="#configA"></sk-app>
<sk-config
        theme="jquery"
        base-path="/src"
        lang="en"
        id="configB"
        routes='{"page-a": "page-a-confb", "page-b": "page-a-confb"}'
></sk-config>
<sk-app configSl="#configB"></sk-app>

</body>

Pages can be loaded automatically with the following mapping:

<sk-config
        theme="jquery"
        base-path="/src"
        lang="en"
        id="configA"
        routes='{"page-a": {"PageAConfA": "/examples/spa/page-a-confa.js"}, "page-b": "page-b-confa"}'
></sk-config>

You can attach your code to app route change with events:

<sk-app id="skApp"></sk-app>
<script>
    skApp.addEventListener('skrouteend', function(event) {
        console.log('skrouteend handled', event);
    });
</script>

template

id: SkAppTpl

Template override

You can override any of elements template by placing it into html as template tag with id matching className + 'Tpl'. Also individual template can be specified for every element's instance by placing template as direct child of element

<sk-button id="myButton" button-type="primary">
    Button
    <template id="SkButtonTpl">
        <button type="button" class="my-class">
            My Better Button
        </button>
    </template>
</sk-button>

Template preload

SkElement has preLoadedTpls array of string property. With overriding it with propNames like 'itemTpl' ans so and provide your custom elemnt's class with relevant 'itenTplPath' like properties containing template paths you can tell renderer to preload specified templates before component rendering started. So you will have less async code magic in your component's business-logic.

SkElement.whenRendered() method

If you want to have some code that should be executed only afer component is rendered you can define callback function with whenRendered method. This callback will be called immediately if element is already rendered or attached to element's rendered event. This method can be called only after element is registred with customElements.

    customElements.define('gridy-grid', GridyGrid);
    gridyGrid.whenRendered(function() {
        console.log('gridyGrid.whenRendered');
    });

Note: if you use container elements they mostly will have their rendered state before subelements.

This aproach requires you component to be registered before whenRendered(callback) method can be called, a more safe way is to use Renderer method whenElRendered(el, callback)

    export class MyElement extends SkElement { 
        connectedCallback() {
            super.connectedCallback();
            let subEl = this.el.querySelector('sk-subelement');
            this.renderer.whenElRendered(subEl, () => {
                // something needed to performed only after rendering is done
            });         
        }
    }

whenElRendered() also has static version

    let el = document.querySelector('sk-someelement');
    Renderer.whenElRendered(el, () => {
        // something needed to performed only after rendering is done
    });         

this method returns Promise that is resolved after callback is called with callback return value as resolve result, callback definition can be omitted

    let el = document.querySelector('sk-someelement');
    Renderer.whenElRendered(el).then(() => {
        // something needed to performed only after rendering is done
    });         

onRendered() is and alternative to whenRendered() based on jquery's deferred

    let el = document.querySelector('sk-someelement');
    el.onRendered((el) => {
        console.log('on rendered', el);
    });

Render auto binds

Sk Elements support auto rerendering bindings with specially prefixed class names.

Use classes with name sk-prop-in-{prop-name} to bind element's innerHTML update to class property change.

Use classes with name sk-prop-at-{prop-name} to bind element's attribute update to class property change. Sometimes most of attributes named with the same name like 'value'. To deail with that it is also possible to define attribute name as a part of property name using _ separator, e.g. sk-prop-at-{prop-name_attr} will map propName_attr class property to element's attribute with name attr.

Use classes with name sk-prop-cb-{prop-name} to bind callback to class property change. Method with name "on{PropName}Change" will be called with binded element and value as arguments.

Note: class names are translated to property names just as it's done for data attributes, e.g. prop-name suffix will be matched with propName class property.

If element instance property is not defined, it will be defined with setter that uses _prefixed class property to store value. Properties are binded to objects (class instances), not to classes (static values).

<sk-button id="myButton" button-type="primary">
    Button
</sk-button>
<template id="SkButtonTpl"><link rel="stylesheet" href="{{ themePath }}/antd.min.css">
    <link rel="stylesheet" href="{{ themePath }}/antd-theme.css">
    <button type="button" class="ant-btn">
        <slot name="label" class="sk-prop-in-my-label"></slot>
        <slot></slot>
    </button>
</template>
<script>
    var i = 0;
    setInterval(() => {
        i++;
        myButton.myLabel = i;
    }, 1000);
</script>

You may meet unpredictiable behaviour when using slots, as this is acutally alternative way to change inner contents, but you still can override element's template without slots:)

Using Dependency Injector

To be happy with structuring complex architectures you may need to use tools like IoC Container with Dependency Injector. You can freely use 3rd party containers, e.g. injection-js. Skinny Widgets include utilities library complets that already has competitive injector that is a bit easier to use than injection-js. Let's see by example. You may find full code samples in examples/ subfolder.

<sk-config
        theme="antd"
        base-path="/node_modules/skinny-widgets/src"
></sk-config>

<script src="/node_modules/skinny-widgets/dist/sk-compat-bundle.js"></script>
<script type="module">
    import { CompRegistry } from "/node_modules/sk-core/src/comp-registry.js";
    import { MyDialog } from "./my-dialog.js";
    import { MyView } from "./my-view.js";

    window.registry = window.registry || new CompRegistry();

    registry.wire({
        SkConfig: { def: SkConfig, is: 'sk-config'},
        SkButton: { def: SkButton, is: 'sk-button'},
        DialogCfg: { buttonSl: '#dialogButton', dialogSl: '#dialog' },
        MyDialog: { def: MyDialog, is: 'my-dialog' },
        MyView: { def: MyView, deps: { dialogCfg: 'DialogCfg', 'foo': 'bar' }, is: 'my-view' },
    });
</script>

<my-view>
    <sk-button id="dialogButton">Open</sk-button>
    <my-dialog id="dialog">This is my dialog</my-dialog>
</my-view>

Here Skinny Widget componets are bootstraped with complets injector. They are coupled with MyView Custom Element that may have the following code.

export class MyView extends HTMLElement {

    bindDialog() {
        if (this.dialogCfg) {
            let button = this.querySelector(this.dialogCfg.buttonSl);
            let dialog = this.querySelector(this.dialogCfg.dialogSl);
            if (button) {
                button.addEventListener('click', (event) => {
                    dialog.open();
                });
            }
        }
    }

    render() {
        this.bindDialog();
    }

    connectedCallback() {
        this.render();
    }
}

MyDialog can be empty class that extends SkDialog.

In the example below selectors for elements for binding are taken from context of html page and not harcoded so you can easily reconfigure them when markup is changed. Also html has absolutely no business logic code.

If element was defined in same registry and definition was met again in will not be wired again. So you can specify the same dependencies to have modularity with autonomous and united working ability. Also you always can register the same classed to different keys and custom-elements with class inheritance e.g. my-button derived from sk-button.

Connections

Connections allow you to wire components without boilerplate in code and markup.

<script type="module">
    import { SkButton } from '/node_modules/sk-button/src/sk-button.js';
    import { SkDialog } from '/node_modules/sk-dialog/src/sk-dialog.js';
    // import { SkConfig } from '/node_modules/skinny-widgets/src/sk-config.js'; // load path specified in registry
    import { CompRegistry } from "/node_modules/sk-core/src/comp-registry.js";
    //import { MyView } from '/my-view.js'; // Inline has been chosen for convinience

    window.registry = window.registry || new CompRegistry();

    import { SkElement } from '/node_modules/sk-core/src/sk-element.js';
    
    class MyView extends SkElement {

    }

    registry.wire({
        SkConfig: { def: SkConfig, is: 'sk-config', path: 'sk-config.js' },
        SkButton: { def: SkButton, is: 'sk-button' },
        SkDialog: { def: SkDialog, is: 'sk-dialog' },
        MyView: {
            def: MyView,
            connections: [
                { from: '#myButton', to: '#myDialog', targetType: 'method', target: 'open' },
                { sourceType: 'prop', source: 'open', to: '#myDialog', targetType: 'method', target: 'open' }, 
                { from: '#myButton2', sourceType: 'event', source: 'click', to: '#myDialog', targetType: 'method', target: 'open' }
            ],
            is: 'my-view'
        },
    });
</script>

from - selector or name for source, set to host element if subelement or one matching in global document was not found by selector, selecting is lazy

sourceType - emitter type can be 'event', 'prop', 'attr' (default: 'event')

source - emitter name (default: 'click')

to - target selector, defaults to host if subelement or one matching in global document was not found by selector, selecting is lazy

targetType - target type, can be 'method', 'event', 'attr', 'togglattr' (value examples: "true", "one|other", empty will remove or add value the same as attribute name), 'prop', 'togglprop', 'function' (default: 'event').

target - target name (default: 'click')

sk-registry

SkRegistry is xhtml wrapper for Dependency Injector made to go happier with markup generators.

<sk-registry>
    <sk-registry-item key="SkConfig" defName="SkConfig" is="sk-config" full-path="/node_modules/sk-core/src/sk-config.js"></sk-registry-item>
    <sk-registry-item key="SkButton" defName="SkButton" is="sk-button" full-path="/node_modules/sk-button/src/sk-button.js"></sk-registry-item>
    <sk-registry-item key="SkDialog" defName="SkDialog" is="sk-dialog" full-path="/node_modules/sk-dialog/src/sk-dialog.js"></sk-registry-item>
    <sk-registry-item key="MyViewCfg" val='{"buttonSl": "#dialogButton", "dialogSl": "#dialog"}'></sk-registry-item>
    <sk-registry-item key="MyView" defName="MyView" is="my-view" plugins='[{"onAttrChange": "onAttrChanged"}]'
                      deps='{"viewCfg": "MyViewCfg"}'>
        <sk-registry-connection from="#myButton" to="#myDialog" target-type="method" target="open"></sk-registry-connection>
    </sk-registry-item>
</sk-registry>

attributes

after-content - load dependencies with 'DOMContentLoaded' event, may be used for static phase loading when not all content may present in DOM.

<script src="/node_modules/sk-jquery-bundle/dist/sk-jquery-bundle.js"></script>
<script src="/node_modules/sk-jquery-bundle/dist/sk-registry-init-bundle.js"></script>

<sk-config
    theme="jquery"
    base-path="/node_modules/sk-core/src"
    theme-path="/node_modules/sk-theme-jquery"
    lang="ru"
></sk-config>
<sk-input>Input</sk-input>
<sk-button>Ok</sk-button>
<sk-registry after-content>
    <sk-registry-item key="SkInput" defName="SkInput" is="sk-input"></sk-registry-item>
    <sk-registry-item key="SkButton" defName="SkButton" is="sk-button"></sk-registry-item>
</sk-registry>

You can init everything you need with just registry by adding sk-registry-init.js and 'sk-auto-reg' key for document or body.

<html class="sk-auto-reg">
<body>
<sk-config
    theme="jquery"
    base-path="/node_modules/skinny-widgets/src"
    lang="ru"
    id="myConfig"
></sk-config>
<script type="module" src="/node_modules/sk-core/src/sk-registry-init.js"></script>
<sk-registry>
    <sk-registry-item key="SkConfig" defName="SkConfig" is="sk-config" path="sk-config.js"></sk-registry-item>
    <sk-registry-item key="SkButton" defName="SkButton" is="sk-button" path="sk-button.js"></sk-registry-item>
    <sk-registry-item key="SkDialog" defName="SkDialog" is="sk-dialog" path="sk-dialog.js"></sk-registry-item>
    <sk-registry-item key="MyViewCfg" val='{"buttonSl": "#dialogButton", "dialogSl": "#dialog"}'></sk-registry-item>
    <sk-rsegistry-item key="MyView" defName="MyView" is="my-view" plugins='[{"onAttrChange": "onAttrChanged"}]'
                      deps='{"viewCfg": "MyViewCfg"}'>
        <sk-registry-connection from="#myButton" to="#myDialog" target-type="method" target="open"></sk-registry-connection>
    </sk-rsegistry-item>
</sk-registry>
</body>
</html>

Run async functions serially

SkElement has helper method runSerial() to run async (parallel) functions one by one

    function asyncTimeout(delay) {
        return (new Promise(resolve => { setTimeout(() => resolve(delay), delay) }))
            .then(d => console.log(`Waited ${d} seconds`));
    }
    this.runSerial([
        asyncTimeout,
        { func: asyncTimeout, args: [ 3000 ] },
        { func: asyncTimeout, args: [ 1000 ] }
    ]);

Plugins

You can develop missing functionality to widgets using inheritance, but plugins seems to be more reusable and abstract way.

    <sk-config 
        theme="jquery"
        base-path="/node_modules/skinny-widgets/src"
    ></sk-config>
    <sk-input id="myInput" name="myInput">My Input</sk-input>
    <script type="module">
        import { SkConfig } from '/node_modules/sk-core/src/sk-config.js';
        import { SkInput } from '/node_modules/sk-input/src/sk-input.js';

        myInput.plugins = [
            { 
                onEventStart: function(event) {
                    console.log('event start hook', event);
                },
                onEventEnd: function(event) {
                    console.log('event end hook', event);
                },
                onAttrChange: function(name, oldVal, newVal) {
                    console.log('attr change hook', name, oldVal, newVal);
                },
                onRenderStart: function() {
                    console.log('render start hook');
                },   
                onRenderEnd: function() {
                    console.log('render end hook');
                }              
            }
        ];
        customElements.define('sk-config', SkConfig);
        customElements.define('sk-input', SkInput);
    </script>

Note: If you want to catch early hooks like 'onRenderStart' consider defining plugin before customElements.define() has been called for element. Note: Do not use arrow functions or this ref inside them

String can be provided instead of function definition, in this case function will be called by name in element class or impl or window.

Configuration for plugin loading can be provided via attributes as follows

    <gridy-chart id="gridyChart2"
         attr-plugins="true" plg-foo-cn="MyPlugin" plg-foo-path="/my-plugin.js"></gridy-chart>

plugins can be defined globally (for every element instance) in sk-config element, note: you may have to disable dynamic imports (with dimports="false" attribute) and define plugin classes globally (defined to window by class name) in case of bundle usage.

Plugin usage example

Init new project and install skinny-widgets and inputmask library into it.

mkdir skinny-widgets-inputmask
cd skinny-widgets-inputmask
npm init --y
npm i skinny-widgets imask --save

create index.html with the following code:

<sk-config
    theme="jquery"
    base-path="/node_modules/skinny-widgets/src"
    id="myConfig"
></sk-config>

<sk-input id="myInput1">Tel</sk-input>

<script src="node_modules/imask/dist/imask.js"></script>
<script type="module">
    import { SkConfig } from '/node_modules/sk-core/src/sk-config.js';
    import { SkInput } from '/node_modules/sk-input/src/sk-input.js';

    myInput1.plugins = [
        {
            onRenderEnd: function(event) {
                IMask(this.impl.inputEl, {
                    mask: '+{7}(000)000-00-00',
                    lazy: false,  // make placeholder always visible
                    placeholderChar: '#'     // defaults to '_'
                });

            }
        }
    ];

    customElements.define('sk-config', SkConfig);
    customElements.define('sk-input', SkInput);
</script>

You can specify plugins with registry

<script type="module">
    import { SkButton } from '/node_modules/sk-button/src/sk-button.js';
    import { SkDialog } from '/node_modules/sk-dialog/src/sk-dialog.js';
    import { SkConfig } from '/node_modules/sk-core/src/sk-config.js';
    import { CompRegistry } from "/node_modules/sk-core/src/comp-registry.js";
    //import { MyView } from '/my-view.js'; // Inline has been chosen for convinience
    
    window.registry = window.registry || new CompRegistry();
    
    import { SkElement } from '/node_modules/skinny-widgets/src/sk-element.js';
    import { SkComponentImpl } from "/node_modules/skinny-widgets/src/impl/sk-component-impl.js";
    
    export class MyView extends SkElement {
    /*    // this may help component behave more like by main theme
       get impl() {
           if (! this._impl) {
               class MyImpl extends SkComponentImpl {
               }
               this._impl = new MyImpl(this);
           }
           return this._impl;
       }*/
    
        static get observedAttributes() {
            return ['open'];
        }
    }
    
    registry.wire({
       SkConfig: { def: SkConfig, is: 'sk-config' },
       SkButton: { def: SkButton, is: 'sk-button' },
       SkDialog: { def: SkDialog, is: 'sk-dialog' },
       MyView: {
           def: MyView,
           connections: [
               { from: '#myButton', to: '#myDialog', targetType: 'method', target: 'open' },
               { sourceType: 'prop', source: 'open', to: '#myDialog', targetType: 'method', target: 'open' }, 
               { from: '#myButton2', sourceType: 'event', source: 'click', to: '#myDialog', targetType: 'method', target: 'open' }
           ],
           plugins: [
                {
                    onAttrChange: function(event) {
                        console.log('attr change hook', event);
                    }
                }
           ]                    
           is: 'my-view'
       },
    });
    myView.addPlugin({
        onAttrChange: function(event) {
            console.log('attr change2 hook', event);
        }
    }, true);
</script>

Note: be carefull with rendering hooks as you may have other class when element is not yet registred for customElements and absolutely other after, so the same example with mixed plugins for onRenderEnd hook will not work as you may expect.

Override jsonpath library examples

<script type="module">
    import { JSONPath } from './node_modules/jsonpath-plus/dist/index-browser-esm.js';
    import { JsonPath } from '/node_modules/sk-core/src/json-path.js';

    const result = JSONPath({path: '$.foo', json: { foo: 'bar'}});
    //JsonPath.querySync = function() {
    JsonPath._impl = new class {
        query() {
            console.log('overriden querySync');
            let path = arguments[1]; // fallback simple impl
            let data = arguments[0];
            return JSONPath({ path: path, json: data });
        }
    }
    console.log('result', result, JsonPath);

</script>

Building

You can rebuild bundle with build command

npm run build

Parameters

--tpl-inline-styles - inline styles from link tags to templates

Development

Add your own element

Sk Widgets allows you to create own subset of themed widgets using inheritance. To figure out how to do that, check out DI example. In short you will have to create own file structure similar to original that may extend built-in themes or base classes. To add new own element, ensure you are done with the following steps:

  1. create element class extending sk-element, override impl getter to fill it with your themes
  2. create common implementation class in impl/ and concrete implementations extending in for every theme you use, you can extend built-in themes and classes, some base methods you may have to override your in each element class to have them working properly.
  3. add templates for each theme and define them in theme's templates.json to be build in template bundles
  4. add element and impl classes to index.js to be build in javascript bundles
  5. add element to sk-bootstrap.js to enable autoloading and register to custom element tags
  6. rebuild project

Add new version tag

git tag -a v0.1.25

fill in comment without v in version number

complets library version upgrade

skinny-widgets has non-npm dependency on complets library that contain some web component and portlet related utilities. To update it to lates version use the following git command:

git submodule update --remote --merge

Tests

Web Component Tests

  1. run http-server on project root
cd skinny-widgets
npm start
  1. open /test/all.html in browser with developer console opened

Code and Design Conventions

  1. Org and naming conventions

1.1 JavaScript is used for development, it's allowed to use other languages with keeping full runtime (browser) and compile time compatibility for library/product level development. Code must work in actual versions of most popular browsers like Chromium and Firefox.

1.2 Classic Camel Case "as in Java" is used for writing code, uppercase for functional constants, internal values are started with single underscore. JavaScript private variables used only in cases when public access is really forbidden.

1.3 let keyword is used for most of variable declarations, const when you can prove real danger of mutability for every case with tests

1.4 Class based structuring is preferred for architecture level, functional is allowed for local and concrete solutions

1.5 When selecting other custom elements, their tag names should be stored in exported constant variables that are defaulting for attribute getters.

export const PAGER_TN = "gridy-pager";


export class GridyTable extends GridyElement {

    get pagerTn() {
       return this.getAttribute('pager-tn') || PAGER_TN;
    }

    set pagerTn(pagerTn) {
       return this.setAttribute('pager-tn', pagerTn);
    }
}

This will allow you to easily override values on both runtime and compile time levels and use names from 3rd party code without duplicating literals and hardcode.

1.6 Names for tag name containing constants and properties and accessors should be ended with Tn suffix.

1.7 Names for variables containing selectors should be ended with Sl suffix

1.8 Names for variables containing selected dom nodes should be edned with El and Els (for many) suffixes.

1.9 Names for variables containing class names should be ended with Cn suffix.

1.10 Theme independent logic is placed in SkElement class inherited from HTMLElement and registered in Custom Elements. Theme dependent logic is placed in Impl companion class.

1.11 Common logic should be placed in service classes injected with registry subsystem or loaded on element's bootstrap with configurable with attributes references.

1.12 Elements should be functionally/bootstrap independent and lose-coupled, but can rely on some general infrastructure performed by SkinnyWidgets like config, logger, renderer.

1.13 Interop between elements is generally organized with browser events, but DOM parent can directly control own children. As for children, they can send events to parent with bubbling.

1.14 npm package for element's general code is named sk-{name}.

1.15 Theme related implementation package is named as sk-{name}-{theme}.

1.16 Theme bundle npm package is named sk-{theme}-bundle.

1.17 Theme npm package is named sk-theme-{theme}.

  1. Solutions

2.1 Lasy element selectors

links to preselected elements should be stored as lazy-accessors. Their names should be added to subEls class and implementations properties. This allows to flush cashes and do other operations on all them in abstract ways.

export class SkSelect extends SkElement {

   get subEls() {
       return [ 'body' ];
   }

   get body() {
       if (! this._body) {
           this._body = this.el.querySelector('select');
       }
       return this._body;
   }

   set body(el) {
       this._body = el;
   }
}

Companion properties are preferred to have public access allowing external manipulations and introspections.

2.2 Templates

2.2.1 All significant markup must be implemented with mounted templates.

2.2.2 Name of template should end with Tpl suffix, e.g. itemTpl, activeLinkTpl ans so on.

2.2.3 Name of template is better to be specified in preLoadedTpls array for preloading.

2.2.4 Paths to template files should be stored as overridable property (getter) ending with suffix Path, e.g itemTplPath, activeLinkTplPath