Web Components are simple to use... but, due to a multitude of non-intuitives behaviors and rules, hard to use correctly. Most of examples and documentation found on the Internet are unsafe (cf List of issues solved by LISS).
LISS enables you to easily use Web Compoments without worrying about all of that.
In order to use LISS in your project, copy either the /index.ts
or /index.js
file into your project.
đź’ˇ If you need to rebuild the JS file, use the command: tsc index.ts --strict --target esnext --module esnext
.
đź’ˇ To run the tests : tsc --strict --noEmit --target esnext --module esnext $FILE
To create a new components, simply create a class extending LISS()
and register it using LISS.define()
:
<!-- cf /examples/basic -->
<!DOCTYPE html>
<html>
<head>
<script type="importmap">
{
"imports": {
"LISS": "$LISS/index.js"
}
}
</script>
<script type="module">
import LISS from 'LISS';
class MyComponent extends LISS() {
// Initialize your WebComponent
constructor() {
super();
// Use this.content to initialize your component's content
this.content.append('Hello World ;)');
console.log('State (initial)', {
// Use this.content to access your component's content:
Content: this.content, // ShadowRoot
// Use this.host to access the component's host:
Host : this.host, // <my-component></my-component>
// Use this.attrs to efficiently access the component's host's attributes:
Attributes: {...this.attrs}, // {}
// Use this.params to access the component parameters.
Parameters: this.params // {}
});
}
}
// Define your WebComponent
LISS.define('my-component', MyComponent); // define the "my-component" component.
</script>
</head>
<body>
<my-component></my-component> <!-- Prints "Hello World ;)" -->
</body>
</html>
đź“– See the auto mode for easier usage.
đź“– And a lot more features and examples below.
In vanilla JavaScript, components shouldn't access the DOM before the first call of connectedCallback()
. This often leads to the creation of an init()
method which is a really bad practice and hampers TS type checking of attributes. Indeed, the component can have its method called before being initialized, requiring safe guards at the start of each methods. For example, attributeChangedCallback()
can be called before initialization, even though attributes shouldn't be accessed before initialization.
Even with safeguards, and an init()
method, errors can still occurs. If the component is defined before the DOM has finished loading, some children can be missing during initialization. customElements.upgrade(this)
might also be required to ensure the children are upgraded. Also, customElements.define()
third argument must match the class inherited by the Web Component (more info) which is redoundant information, can lead to errors, and may be in some cases troublesome to handle.
LISS tacles these issues by constructing the component only when it is fully ready and allows to declare dependancies. LISS also supports giving parameters to the component.
Without LISS:
class Component extends HTMLTableRowElement {
someAttrs?: string; // is undefined before initialization.
#isInit = false;
connectedCallback() {
if(!this.#isInit)
init();
}
attributeChangedCallback(...) {
//...
if( ! this.#isInit )
return;
}
protected init() {
customElements.upgrade(this);
//...
this.someAttrs = "ok";
this.#isInit = true;
//...
}
}
function define() {
customElements.define('my-component', Component, {extends: "tr"});
}
if(document.readyState === "interactive" || document.readyState === "complete")
define();
else
document.addEventListener('DOMContentLoaded', define);
With LISS:
import LISS from "LISS";
class Component extends LISS({host: HTMLTableRowElement}) {
someAttrs: string;
constructor() {
this.someAttrs = "ok";
}
onAttrChanged(...) {
//...
}
}
LISS.define('my-component', Component);
In vanilla Javascript, use of ShadowRoot
is recommanded, however, some custom element inheriting builtin elements doesn't support having one (more info). This leads to different ways of doing things whether the component supports or not ShadowRoot
.
LISS uniformalizes usage independantly of the ShadowRoot
support:
-
By default,
shadowRoot
is created if the component supports it.LISS({shadow:...})
enables to explicitly set theShadowRoot
mode (NONE
,OPEN
, orCLOSE
). -
this.content
enables to access the component content, i.e. theShadowRoot
if exists or the host. -
CSS rules are rewriten to enable usage of
:host
in components withoutShadowRoot
. -
LISS provides methods to simulate
part
andslot
in components withoutShadowRoot
(WIP).
When manipulating the DOM, we manipulate Element
instances that may or may not correspond to a component, which may or may not be defined or initialized. LISS offers some helpers to facilitate Element
manipulation :
Without LISS:
const element = querySelector('...');
if( element === undefined)
throw new Error('...');
await customElement.whenDefined('....')
customElements.upgrade(element); // ensure it is upgraded.
if( element instanceof Component ) {
if( ! element.isInit )
await element.whenInit;
}
With LISS:
const component = await LISS.qs<Component>('....');
// or
const component = await LISS.qs('....', 'my-component'); // for TS
In vanilla JavaScript, building component is troublesome, doesn't allow for parameters, and might lead to errors:
Without LISS:
const element = document.createElement<Component>('tr', {is: 'my-component'});
element.setAttribute('foo', "24");
// SHOULDN'T USE "element"" YET: STILL NOT INITIALIZED!
document.body.append(element); // will initialize it...
element.setAttribute('faa', "42"); // too late for parameters.
With LISS:
const Element = await LISS.build('my-component', {
params: {...},
attrs: {
foo: "24",
faa: "42"
},
parent: document.body
});
You can see all examples below in the LISS/examples/
directory.
- Management of HTML attributes
- Extend JS and HTML classes
- Dynamically build component instances
- Access components through the DOM
- Use HTML/CSS files/strings to fill the component
- Auto mode
- Advanced features
- ShadowRoot helpers
- dependencies
- LISS full API
LISS enables to observe the host HTML attributes, simply by specifying their names when building the component (extends LISS({attributes:[...]})
).
Then, this.onAttrChanged()
will be called at each modification of the observed attributes. If this.onAttrChanged()
returns false, the changed attribute will be reverted to its previous value.
this.attrs
enables to access them in an efficient way, i.e. without requiring multiples access to the DOM. Modification of an attribute through this.attrs
will update the HTML attributes without firing this.onAttrChanged()
.
// cf /examples/attributes
import LISS from 'LISS';
class MyComponent extends LISS({
attributes: ["counter"] // observed attributes.
}) {
#interval = null;
constructor() {
super();
// this.attrs contains the current values of the observed attributes.
console.log("Attributes (initial)", {...this.attrs});
// you can validate this.attrs here.
this.#counter = setInterval( () => {
// will trigger onAttrChanged
this.host.setAttribute("counter", +this.attrs.counter+1);
}, 1000);
// will NOT trigger onAttrChanged.
this.content.textContent = this.attrs.counter = 0;
}
onAttrChanged(name, oldValue, newValue) {
console.log("AttrChanged", name, oldValue, "->", newValue);
console.log("Attributes (now):", {...this.attrs});
// you can validate this.attrs here.
if( this.attrs.counter === "5" ) {
clearInterval(this.#interval);
return false; // cancel the change.
}
this.content.textContent += this.attrs.counter;
}
}
LISS.define('my-component', MyComponent);
<my-component counter="null"></my-component><!-- prints 01234 -->
LISS()
allows you to extends existing classes thanks to the extends
(JS classes) and host
(HTMLElement classes) options.
đź’ˇ We also provide an interface EventsTarget
to better ensure events types when using events and listeners.
// cf /examples/extend
import LISS from 'LISS';
class MyComponent extends LISS({
host : HTMLTableRowElement,
extends: EventTarget, // the component is able to send events.
// in TS, to ensure events types, add: as EventsTargetCstr<{"event_name": detail_type}>
content: "<td>Hello World ;)</td>"
}) {
constructor() {
super();
this.host.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('click', {detail: null}));
})
}
}
// Define your WebComponent
LISS.define('my-component', MyComponent);
const component = await LISS.qs( LISS.selector("my-component") );
component.addEventListener('click', () => {
alert('click');
});
<table>
<tr is="my-component"></tr>
</table>
LISS.build()
enables you to build new component instances through many options:
// cf /examples/build
import LISS from 'LISS';
class MyComponent extends LISS({
css: ":host{ color: var(--color) }",
params: { // default values
foo: 1, // in TS add: as number
faa: 1,
fuu: 1
}
}) {
constructor() {
super();
console.log('State (initial)', {
Content: this.content, // ShadowRoot
Host: this.host, // <my-component></my-component>
Attributes: {...this.attrs}, // {}
Parameters: this.params // {foo:1,faa:2,fuu:3}
});
}
}
LISS.define('my-component', MyComponent, {params: {faa: 2}});
const elem = await LISS.build('my-component', {
params: {fuu: 3}, // component parameters
// initialize: true, // force initialization of element before insertion.
content: "Hello ;)", // set host children
// or
// content: ["Hello", "World"],
parent : document.body, // add component to the parent
id : "myWebComponent", // set host ID.
classes: ["c1", "c2"], // set host classes
cssvars: {"color": "blue"}, // set host CSS variables
attrs: {attrname: "value"}, // set host attributes
data : {name : "value"},
listeners: { // set host listeners
"click": () => { alert('click!') }
}
});
đź’ˇ Parameters can also be specified in LISS()
and LISS.define()
options.
đź“– See also the full API documentation
LISS provides several tools to securely manipulate components through the DOM:
-
from an
HTMLElement
:LISS.getLISS<T extends LISSBase<>>(elem: HTMLElement): Promise<T>
-
from a query string:
LISS.qs<T extends LISSBase<>>('...'): Promise<T>
đź’ˇ LISS also provide a function overload enabling better type checking in TS.
LISS()
allows to inject HTML and CSS files/strings into your component thanks to the content
and css
options:
// cf /examples/inject-html-css/
import LISS from "LISS"
const CSS_RULES = `
:host {
color: blue;
}
`;
export default class Component extends LISS({
content: fetch("./component.html"), // string|Response|HTMLTemplateElement or a Promise of it.
css : [fetch('./component.css'), CSS_RULES] // string|Response|HTMLStyleElement|CSSStyleSheet or a Promise of it, or an array of it.
}) {
//...
}
LISS.define("my-component", Component);
LISS can also automatically build and import your components, making them even easier to use.
This feature is enabled simply by adding a <liss-auto src='$COMPONENTS_DIR'></liss-auto>
HTML tag into your webpage:
<!-- cf /examples/liss-auto/ -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LISS Auto</title>
<script type="module" src='$LISS' defer></script>
</head>
<body>
<liss-auto src="./components/"></liss-auto>
<!-- some components -->
<liss-html></liss-html>
<liss-css></liss-css>
<liss-js></liss-js>
</body>
</html>
In auto-mode, a component <$name></$name>
must be defined in the $COMPONENTS_DIR/$name
directory.
For example, the component <liss-html></liss-html>
will be defined in the ./components/liss-html/
directory.
The component directory must at least include either an index.js
or an index.html
file.
An optionnal index.css
file can also be provided.
âš In order to suppress 404 errors in the console, auto-mode requires to put the file $LISS/sw.js
in the same directory than your webpage.
Defining a component with only an HTML file is very easy with LISS: simply create a $COMPONENTS_DIR/$name/index.html
file with the component's HTML content:
<!-- cf /examples/liss-auto/components/liss-html/index.html -->
Hello World
Will define the component <liss-html></liss-html>
containing Hello World
:
<liss-html></liss-html> <!-- will print "Hello World" -->
You can also add a CSS file to your component, simple by adding a $COMPONENTS_DIR/$name/index.css
file containing rules starting with :host
:
:host {
color: blue;
}
You can also define a component with only a JS file, by creating a $COMPONENTS_DIR/$name/index.js
file default exporting a function returning a class extending LISS()
:
// cf /examples/liss-auto/components/liss-js/index.js
import LISS from "LISS";
export default function(options) {
return class LISSComponent extends LISS(options) {
constructor() {
super();
//do stuff here (cf LISS features)
}
}
}
You can also add an index.html
and a index.css
files to your component. LISS will then automatically use them to define your component's initial content. Their content are given by the options
parameter.
When using a close ShadowRoot
, we still might want to use global CSS rules or global delegated event listeners.
<div class="fullscreen-onclick"></div>
// applies tho CSS rules to all components and to the document.
LISS.insertGlobalCSSRules(`.fullscreen-onclick {
//...
}`);
// listen to the click events inside all components and inside the document.
LISS.insertGlobalDelegatedListener("click", ".fullscreen-onclick", (ev) =>
{
//...
});
When using elem.closest(selector)
inside a ShadowRoot
, the ancestors of the ShadowRoot
aren't checked. We provide LISS.closest<T>(selector, elem)
in order to check them.
<div class="fullscreen_target">
<my-toolbar>
<fullscreen-btn></fullscreen-btn>
</my-toolbar>
</div>
function onFullscreenBtnClicked(ev) {
ev.target.closest(".fullscreen_target"); // may not be found.
LISS.closest(".fullscreen_target", ev.target); // found.
}
This function awaits the component's dependancies, then declares a new custom element using customElements(tagname, _host, ...)
.
Internally, _host
is an instance of LISSHost<>
which, once the custom element is ready to be initialized, instantiate a new instance of the given ComponentClass
.
Name | Type | Description |
---|---|---|
tagname |
string |
|
ComponentClass |
LISSReturnType<Extends, Host, Attrs, Params> |
A class extendings a class returned by LISS() . |
options.dependancies |
readonly Promise<string>[] |
Promises to wait before declaring the component. |
options.params |
Partial<Params> |
Parameters to add to the component constructor call. |
LISS also provides functions to follow the component definition process/status:
LISS.isDefined (tagname: string): boolean;
LISS.whenDefined(tagname: string,
callback ?: () => void): Promise<void>
LISS.whenAllDefined(tagnames: readonly string[], callback ?: () => void): Promise<void>
Name | Type |
---|---|
options |
LISSOptions<Extends, Host, Attrs, Params> |
return |
LISSReturnType<Extends, Host, Attrs, Params> |
Name | Type | Default | Description |
---|---|---|---|
Extends |
extends Class |
||
Host |
extends HTMLElement |
||
Attrs |
extends string |
||
Params |
extends Record<string, any> |
||
CSSSource |
string|Response|HTMLStyleElement|CSSStyleSheet |
||
extends? |
Constructor<Extends> |
Object |
The JS class the component extends. |
host? |
Constructor<Host> |
HTMLElement |
The host HTML Element class. |
attributes? |
readonly Attrs[] |
[] |
The names of the host HTML attributes to observe. |
params? |
Params |
{} |
Default values for the component parameters. |
dependancies? |
readonly Promise<any>[] |
[] |
Promises to wait before declaring the component. |
content? |
string|Response|HTMLTemplateElement |
undefined |
The component default HTML content. |
css? |
readonly CSS_Source[] | CSS_Source |
[] |
CSS rules for the component. |
shadow? |
ShadowCfg |
closed  or none |
ShadowRoot configuration (0 if none). |
đź’ˇ css
and content
also accept a Promise
.
Methods:
Name | Parameters | Return | Description |
---|---|---|---|
constructor |
this or Promise<this> |
Async constructors are supported. | |
onAttrChanged |
name: string oldValue: string newValue: string |
void or false |
Called when an attribute is changed. Return false to cancel the change. |
Properties:
Modifiers | Name | Type | Description |
---|---|---|---|
readonly public |
host |
Host |
The component HTML host. |
protected readonly |
content |
HTMLElement|ShadowRoot |
The component HTML content. |
protected readonly |
attrs |
Attrs |
The host observed HTML attributes. |
protected readonly |
params |
Params |
The component parameters. |
type LISSBase<E,I,A,P> = InstanceType<LISSReturnType<E,I,A,P>>
Build a new component instance.
Name | Type | Default | Description |
---|---|---|---|
T |
extends LISSBase<E,I,A,P> |
||
tagname |
string |
||
options.initialize |
boolean |
true |
Force the component initialization. |
options.params? |
Partial<Params> |
{} |
Component parameters. |
options.content? |
string|Node|readonly Node[] |
undefined |
Host default content. |
options.id? |
string |
undefined |
Host id attribute |
options.classes? |
readonly string[] |
[] |
Host class list. |
options.cssvars? |
Readonly<Record<string, string>> |
{} |
Host CSS variables. |
options.attrs? |
Readonly<Record<string, string|boolean>> |
{} |
Host HTML attributes. |
options.data? |
Readonly<Record<string, string|boolean>> |
{} |
Host dataset. |
options.listeners? |
Readonly<Record<string, (ev: Event) => void>> |
{} |
Host events listeners. |
options.parent? |
HTMLElement |
undefined |
Element to which append the component. |
Function | Return | Description |
---|---|---|
LISS.getLISS<T>(element) |
Promise<T> |
Returns the LISS component associated to the HTML Element. |
LISS.getLISSSync<T>(element) |
T |
Throws an exception if component not yet initialized. |
LISS.initialize<T>(element, params?) |
Promise<T> |
Force the component initialization. |
LISS.getName(element) |
string |
Returns the component name. |
LISS.selector(name) |
string |
Returns the CSS selector for the given component name. |
parameters
Name | Type | Description |
---|---|---|
T |
extends LISSBase<> |
|
element |
HTMLElement |
|
params? |
Partial<Params> |
LISS provides several fonctions to get fully intialized LISS components from a query string:
Function | Return | Remarks |
---|---|---|
LISS.qs<T>(query, parent?) |
Promise<T> |
Throws an exception if not found. |
LISS.qso<T>(query, parent?) |
Promise<T>|null |
null  if not found. |
LISS.qsa<T>(query, parent?) |
Promise<T[]> |
|
LISS.qsc(query, element) |
Promise<T>|null |
|
LISS.qsSync(query, parent?) |
T |
Throws an exception if component not yet initialized. |
LISS.qsaSync(query, parent?) |
T[] |
Throws an exception if any found component not yet initialized. |
LISS.qscSync(query, element) |
T |
Throws an exception if component not yet initialized. |
parameters
Name | Type | Default |
---|---|---|
T |
T extends LISSBase<any,any,any> |
|
query |
string |
|
parent? |
Element|Document|DocumentFragment |
document |
element |
Element |
đź’ˇ For better type checking in TS, we provide an overload for each of these functions enabling to use a component name as a 2nd parameter:
LISS.qs<T extends keyof Components>(selector: string,
tagname: T,
parent ?: ...): Promise<Components[T]>
// To use your own components, declare them:
LISS.define('my-component', Component);
declare module '$LISS' {
interface Components {
'my-component': Component
}
}
// selector = `${selector}${LISS.selector('my-component')}`
// selector = "body > :is(my-component,[is="my-component"])"
LISS.qs('body > ', 'my-component'); // Promise<Component>
Function | Return | Description |
---|---|---|
LISS.closest<T>(css_selector, elem) |
T |
Like elem.closest()  but traverses ShadowRoot . |
LISS.insertGlobalCSSRules(css) |
Add the css  rules to all components and to the document. |
|
LISS.insertGlobalDelegatedListener(evt, selector, handler) |
Add a delegated listener to all componets and to the document. |
<!-- LISS/examples/parts.html -->
<script type="module">
import LISS from './LISS/index.js';
class MyComponentA extends LISS() {
constructor(htmltag) {
super(htmltag);
const span = document.createElement('span');
span.setAttribute('part', 'foo');
this.content.append(span);
}
}
class MyComponentB extends LISS(HTMLTableRowElement) {
constructor(htmltag) {
super(htmltag);
const td = document.createElement('td');
td.setAttribute('part', 'foo');
this.content.append( td );
}
}
LISS.define('my-component-a', MyComponentA);
LISS.define('my-component-b', MyComponentB);
await LISS.whenDefined('my-component-a');
let elem = document.querySelector(`my-component-a`);
let part = elem.getPart("foo");
part.textContent = "Hello";
console.log("A", elem.getParts("foo"));
await LISS.whenDefined('my-component-b');
elem = document.querySelector(`tr[is="my-component-b"]`);
part = elem.getPart("foo");
part.textContent = "World";
console.log("B", elem.getParts("foo"));
</script>
<my-component-a></my-component-a>
<table>
<tr is="my-component-b"></tr>
</table>
-
Dependancies : after Host decl.
-
Expect parents/children initialization options (2 incompatibles) -> expect Host ? -> DOM connect/disconnect => different life-cycle / upper life-cycle ? -> add doc for init/life cycle
-
npm package / distribute
-
Write doc for
- onDOM(Dis)Connected
- LISS.extends
- attrs default value.
- setAttrDefault
- LISSParams
-
ShadowRoot parts