Skip to main content

A Vainilla Library for Web Components

Open in Visual Studio Code npm license npm npm

Why “LS-Element?”

LS-Element React StencilJS VanillaJS
Avoids Virtual Dom
Templates with JSX
Differentiation between attributes and properties in jsx
Standard Web Components
Observables / stores support
Esbuild as default bundler
TypeScript support
Reactive
Styling / Constructable Stylesheets support
Automatic type generation
Without polyfills
Attributes / Native events support
Supports Shadow DOM
Supports Custom Built-in elements
Can be used with different frameworks right out of the box
✅ = implemented
⭕ = partially implemented
❌ = not implemented

Getting Started

You can use this template or you can see on Code Sandbox.

Creating components

LS-Element custom elements are plain objects.

New components can be created using the jsx/tsx extension, such as MyCounter.tsx.

import { AdoptedStyle, createCustomElement, EventDispatcher, h } from "@lsegurado/ls-element";
import { counterStyle } from "./counterStyle";

export const MyCounter = createCustomElement('my-counter', {
  reflectedAttributes: {
    count: 0
  },
  methods: {
    decrementCount() { this.count-- },
    incrementCount() { this.count++ },
  },
  events: {
    countChanged: new EventDispatcher<number>()
  },
  observe: {
    count() {
      this.countChanged(this.count)
    }
  },
  render() {
    return (
      <>
        <AdoptedStyle id="style">{counterStyle}</AdoptedStyle>
        <button id="decrement-count" onpointerup={this.decrementCount}>-</button>
        <span id='count'>{this.count}</span>
        <button id="increment-count" onpointerup={this.incrementCount}>+</button>
      </>
    )
  }
})

Note: the .tsx extension is required, as this is the standard for TypeScript classes that use JSX.

To use this component, just use it like any other HTML element:

import '../Counter';

<my-counter id="my-counter" oncountchanged={(ev) => console.log(`New count value: ${ev.detail}`)} />

Or if you are using jsx

import Counter from '../Counter';

<Counter id="my-counter" oncountchanged={(ev) => console.log(`New count value: ${ev.detail}`)} />

Please note that all elements included in the components in this library require an ID to work properly. This allows avoiding the use of the virtual DOM.

How this works?

When you update an item, the library looks for your changes and only updates the attributes / children / etc that really changed (similar to how the virtual DOM works). By forcing the use of IDs it is easy to find changes and update them without replacing nodes in the DOM and without using the virtual DOM.

But… Wait, DOM elements shouldn’t be unique?

In this case I am going to quote Eric Bidelman, a Google engineer on this topic: For example, when you use a new HTML id/class, there’s no telling if it will conflict with an existing name used by the page. That is to say that while it is inside the Shadow DOM you should not worry about if your ID is repeated with one outside the Shadow DOM.

Why not use keys like React?

Searching with IDs is so much faster than searching by queryselector. You can look at this topic and this another

But… What if I don’t want to use Shadow DOM?

You can use our id generator to create unique IDs for each element in your component with a discernible key.

render() {
    return (
        <>
            <style id={this.idGen('style')}>{style}</style>
            <button id={this.idGen('decrement-count')} onpointerup={this.decrementCount}>-<button>
        </>
    );
}

The result will be like this:

<style id="093dc6b7-315d-43c1-86ef-fcd49130ea32"></style>
<button id="c8d61264-45ee-42ce-9f74-1d76402d1f48">-</button>

Component structure

A component consists of the following properties:

Property Description
attributes Allows to define attributes.
reflectedAttributes Allows to define reflected attributes and follows the Kebab case.
transactions Transactions are functions that notify changes at the end of the transaction.
methods Methods are functions that notify changes at the time of making the change.
render Function that renders the component.
observe Contains methods with a name of an attribute / reflected attribute / observable like. Those methods are executed when a change has been made to their corresponding property.
lifecycle
willMount This method is called right before a component mounts.
didMount This method is called after the component has mounted.
didUnmount This method is called after a component is removed from the DOM.
willUpdate This method is called before re-rendering occurs.
didUpdate This method is called after re-rendering occurs.
willReceiveAttribute This method is called before a component does anything with an attribute.
events Allows you to define an event to his parent and triggering it easily. It will be defined using Lower case. For example countChanged will be registered as countchanged.
subscribeTo Allows you to subscribe to an observable like (like a store). When the store emit an event, the custom element will be re-rendered.
shadow Allows you to add a Shadow DOM. By default, it uses open mode on Autonomous Custom elements and does not use Shadow DOM on Customized built-in elements. Only this elements are allowed to use Shadow DOM.

Also, you have to create an Autonomous custom element with a tag or in case you want to create an Customized built-in element you have to declare the tag, the class you want to extend and the tag to extend.

LSStore structure

A store consists of the following properties:

Property Description
state Allows to define the store state.
transactions Transactions are functions that notify changes at the end of the transaction.

LSStores use proxies to listen for changes in their state, in addition, they are observable. Each component has an LSStore to listen for changes in its state.

CSS

To use css we provide functions to create Constructable Stylesheets.

createStyleSheet

Allows to create a Constructable Stylesheet with a CSSObject

export const counterStyle = createStyleSheet({
  ':host': {
    display: 'flex',
    flexDirection: 'row'
  },
  span: {
    minWidth: '60px',
    textAlign: 'center'
  }
});

css

Allows to create a Constructable Stylesheet with a Template String. Recomended extension for VSCode.

export const counterStyle = css`
  :host {
      display: flex;
      flex-direction: row;
  }

  span {
      min-width: 60px;
      text-align: center;
  }

CSS module scripts

We do not provide support for this functionality yet as ESBuild does not support it yet. You can read how it works here

Components

Constructable Stylesheets

If you are not familiar with Constructable Stylesheets please check this link. To use Constructable Stylesheets simply import AdoptedStyle and use it like an style tag (see example). In case your browser doesn’t support this feature, it will return a style tag. Remember that you need to use Shadow DOM to be able to use Constructable Stylesheets.

Host

Allows to set attributes and event listeners to the host element itself.

AsyncComponent

Create a component whose content will load after the promise ends. In the meantime you can choose to show a load component or not show anything.

Provides the ability to move around the web page without reloading the page. It uses the same attributes as an anchor tag but also allows the use of URL objects. Uses the goTo method.

Custom element methods

child

Allows to get a child element from the host with the id

rerender

Forces the element to re-render

idGen

Create unique IDs with a discernible key

Attributes vs Properties in jsx

Usually, if you want to get an html like this:

<div class='test'></div>

In React / Stencil / etc you should write a jsx like this:

() => <div className='test'></div>

And eventually code like this would be executed:

const el = document.createElement('div');
el.className = 'test';

In LS-Element you have the freedom to use both attributes and properties and the result will be the same:

// Using properties
() => <div _className='test'></div>
// Using attributes
() => <div class='test'></div>

And eventually code like this would be executed:

const el = document.createElement('div');
// Using properties
el.className = 'test';
// Using attributes
el.setAttribute('class', 'test')

In this way the jsx syntax of LS-Element is more similar to html.

Routing

The intention of using a custom routing tool is to avoid the use of strings to represent the urls and to use modern apis that allow the use of the URL object itself. It also allows to separate the components of the routes which allows a cleaner code.

Note: This is still a work in progress and may change in the future.

const Redirect = () => {
  goTo(urls.syncRoute())
  // Will generate and go to this url: /sync-route
  return <></>
}

//Parent routes
export const { urls, Router, components } = registerRoutes({
  syncRoute: createRoute({
    /**The component to display */
    component: <div id="test3">Hello World</div>,
    title: 'Sync title'
  }),
  //Redirect route
  '/': createRoute({
    component: <Redirect />
  }),
});

//Child routes
export const { urls: urlsChild, Router: RouterChild } = registerRoutes({
  // Async route
  asyncChildRoute: createAsyncRoute<{ searchParam1: string, searchParam2: number }, '#hash1' | '#hash2'>()({
    /** The promise to wait */
    promise: () => import('./AsyncChildExample'),
    /** The component key (by default is default)*/
    key: 'AsyncChildExample',
    /**The title of the page */
    title: 'Async Page title'
    /**The component to display while the promise is loading */
    loadingComponent: <span>Loading...</span>
  }),
  //The parent route
}, urls.syncRoute);

urlsChild.childRoute({ searchParams: { searchParam1: 'param 1', searchParam2: 2}, hash: '#hash1' })
// Will generate this url: /sync-route/async-child-route?searchParam1=param+1&searchParam2=2#hash1

Router and RouterChild are components that represent the mount points of each registered route.

The “components” function is a utility to create asynchronous components that includes the search params and component hashes with the types that were defined when the route was registered

export const AsyncChildExample = components.childRoute(({ searchParams, hash }) => {
  return (
    <>
      {/* Will show the value of searchParam1 */}
      <div id="example">{searchParams.searchParam1}</div>
      {/* Will show true if the hash is #hash1 */}
      <div id="example2">{hash['#hash1']}</div>
    </>
  );
});

Performance optimizations

If you want to help the library to make some optimizations you can use the following attributes:

  • _dynamicAttributes: An array with the names of the attributes that can change.
  • _staticChildren: It indicates that children never change. If you use static Children, there is no need to use _staticChildren or _dynamicAttributes on your children.

Limitations

Observable objects

Because some objects are not proxy compatible we limit the observable objects to:

  • Arrays
  • Dates
  • Maps
  • Sets
  • Any object whose prototype is Object

Polyfills

If you REALLY need polyfills i recommend you to read this topics:

Browser Support

Customized built-in elements

Autonomous custom elements

Compatibility with frameworks

Supporting LS Element

Sponsors

Support us with a donation and help us continue our activities here.

Contributors

License