Web Component

The WebComponent package is a Web Component version of Markup and its built on it as well.

The WebComponent is a single API that allows you to declare native web components enhanced with Markup with things like:

Here is a simple example of a CounterApp with all the main bells and whistles.

typescript
1import { WebComponent, html } from '@beforesemicolon/web-component'2import stylesheet from './counter-app.css' with { type: 'css' }3 4interface Props {5    label: string6}7 8interface State {9    count: number10}11 12class CounterApp extends WebComponent<Props, State> {13    static observedAttributes = ['label']14    label = '+' // defined props default value15    initialState = {16        // declare initial state17        count: 0,18    }19    // attach style at component level20    stylesheet = stylesheet21 22    countUp = (e: Event) => {23        e.stopPropagation()24        e.preventDefault()25 26        this.setState(({ count }) => ({ count: count + 1 }))27        this.dispatch('click')28    }29 30    render() {31        return html`32            <p>${this.state.count}</p>33            <button type="button" onclick="${this.countUp}">34                ${this.props.label}35            </button>36        `37    }38}39 40// define components as you would natively41customElements.define('counter-app', CounterApp)

In your HTML, you can simply use the tag normally.

html
1<counter-app label="count up"></counter-app>

Installation

Via npm:

1npm install @beforesemicolon/web-component

Via yarn:

1yarn add @beforesemicolon/web-component

Via CDN:

html
1<!-- use the latest version -->2<script src="https://unpkg.com/@beforesemicolon/web-component/dist/client.js"></script>3 4<!-- use a specific version -->5<script src="https://unpkg.com/@beforesemicolon/web-component@0.0.4/dist/client.js"></script>6 7<!-- link you app script after -->8<script>9    const { WebComponent } = BFS10    const { html, state } = BFS.MARKUP11</script>

Create component

To create a component, all you need to do is create a class that extends WebComponent then define it.

javascript
1class MyButton extends WebComponent {}2 3customElements.define('my-button', MyButton)

Config

You can configure very few basic things about your component that will determine how your component will be rendered.

ShadowRoot

By default, all components you create add a ShadowRoot in open mode.

If you don't want ShadowRoot in your components, you can set the config.shadow property to false.

javascript
1class MyButton extends WebComponent {2    config = {3        shadow: false,4    }5}6 7customElements.define('my-button', MyButton)

ShadowRoot options

You can specify any attachShadow options in the config object, and they are all optional. The default matches their native default values.

javascript
1class MyButton extends WebComponent {2    config = {3        // whether to attach shadow root4        shadow: true,5        // shadow root options6        mode: 'open',7        delegatesFocus: false,8        clonable: false,9        serializable: false,10        slotAssignment: 'named',11    }12}13 14customElements.define('my-button', MyButton)

Render

Not all components need an HTML body but in case you need one, you can use the render method to return either a Markup template, a string, or a DOM element.

javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class MyButton extends WebComponent {4    render() {5        return html`6            <button type="button">7                <slot></slot>8            </button>9        `10    }11}12 13customElements.define('my-button', MyButton)

In the render method, you can return anything: a string, a DOM Node, a Markup template, a null value, or nothing at all. Some components can just handle some internal logic and don't need to render anything but their tags.

Stylesheet

You have the ability to specify a style for your component either by providing a CSS string or a CSSStyleSheet.

javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2import buttonStyle from './my-button.css' with { type: 'css' }3 4class MyButton extends WebComponent {5    stylesheet = buttonStyle6}7 8customElements.define('my-button', MyButton)

Where the style is added will depend on whether the shadow option is true or false. If the component has a shadow style, it will be added to its own content root. Otherwise, style will be added to the closest root node in which the component was rendered. It can be the document itself or root of an ancestor web component.

css

You can use the css utility to define your style inside the component as well.

javascript
1class MyButton extends WebComponent {2    stylesheet = css`3        :host {4            display: inline-block;5        }6        button {7            color: blue;8        }9    `10}11 12customElements.define('my-button', MyButton)

This makes your style reactive and it helps your IDE give you better CSS syntax highlighting and autocompletion;

javascript
1class MyButton extends WebComponent {2    static observedAttributes = ['variant']3 4    variant = 'primary'5 6    stylesheet = css`7        button {8            color: ${when(is(this.props.variant, 'primary'), 'red', 'blue')};9        }10    `11}12 13customElements.define('my-button', MyButton)

updateStylesheet

You can always manipulate the stylesheet property according to the CSSStyleSheet properties. For when you want to replace the stylesheet completely with another, you can use the updateStylesheet method and provide either a string or a new instance of CSSStyleSheet.

This method is great if you want to dynamically swap a component stylesheet to another. We recommend using the css util to create reactive stylesheets.

Props

If your component expects props (inputs), you can set the observedAttributes static array with all the attribute names.

javascript
1class MyButton extends WebComponent {2    static observedAttributes = ['type', 'disabled', 'label']3}4 5customElements.define('my-button', MyButton)

To define the default values for your props, simply define a property in the class with the same name and provide the value.

javascript
1class MyButton extends WebComponent {2    static observedAttributes = ['type', 'disabled', 'label']3    type = 'button'4    disabled = false5    label = ''6}7 8customElements.define('my-button', MyButton)

To read your reactive props you can access the props property in the class. This is what it is recommended to be used in the template if you want the template to react to prop changes. Check the templating section for more.

typescript
1interface Props {2    type: 'button' | 'reset' | 'submit'3    disabled: boolean4    label: string5}6 7class MyButton extends WebComponent<Props, {}> {8    static observedAttributes = ['type', 'disabled', 'label']9    type = 'button'10    disabled = false11    label = ''12 13    constructor() {14        super()15 16        console.log(this.props) // contains all props as getter functions17        this.props.disabled() // will return the value18    }19}20 21customElements.define('my-button', MyButton)

State

The state is based on Markup state, which means it will pair up with your template just fine.

initialState

To start using state in your component, simply define the initial state with the initialState property.

typescript
1interface State {2    loading: boolean3}4 5class MyButton extends WebComponent<{}, State> {6    initialState = {7        loading: false,8    }9}10 11customElements.define('my-button', MyButton)

setState

If you have a state, you will need to update it. To do that, you can call the setState method with a whole or partially new state object or simply a callback function that returns the state.

typescript
1interface State {2    loading: boolean3}4 5class MyButton extends WebComponent<{}, State> {6    initialState = {7        loading: false,8    }9 10    constructor() {11        super()12 13        this.setState({14            loading: true,15        })16    }17}18 19customElements.define('my-button', MyButton)

If you provide a partial state object, it will be merged with the current state object. No need to spread state when updating it.

You can also provide a callback so you can access the current state data.

typescript
1this.setState((prev) => ({2    loading: !prev.loading,3}))

Events

Components can dispatch custom events of any name and include data. For that, you can use the dispatch method.

javascript
1class MyButton extends WebComponent {2    handleClick = (e: Event) => {3        e.stopPropagation()4        e.preventDefault()5 6        this.dispatch('click')7    }8 9    render() {10        return html`11            <button type="button" onclick="${this.handleClick}">12                <slot></slot>13            </button>14        `15    }16}17 18customElements.define('my-button', MyButton)

This dispatch method also takes a second argument, which can be the data you want to expose with the event.

javascript
1this.dispatch('change', { value })

Lifecycles

You could consider the constructor and render method as some type of "lifecycle" where anything inside the constructor happen when the component is instantiated and everything in the render method happens before the onMount.

onMount

The onMount method is called whenever the component is added to the DOM.

javascript
1class MyButton extends WebComponent {2    onMount() {3        console.log(this.mounted)4    }5}6 7customElements.define('my-button', MyButton)

You may always use the mounted property to check if the component is in the DOM or not.

You have the option to return a function to perform cleanups, which is executed like onDestroy.

javascript
1class MyButton extends WebComponent {2    onMount() {3        return () => {4            // handle cleanup5        }6    }7}8 9customElements.define('my-button', MyButton)

onDestroy

The onDestroy method is called whenever the component is removed from the DOM.

javascript
1class MyButton extends WebComponent {2    onDestroy() {3        console.log(this.mounted)4    }5}6 7customElements.define('my-button', MyButton)

onUpdate

The onUpdate method is called whenever the component props are updated via the setAttribute or by changing the props property on the element instance directly.

javascript
1class MyButton extends WebComponent {2    onUpdate(name: string, newValue: unknown, oldValue: unknown) {3        console.log(`prop ${name} updated from ${oldValue} to ${newValue}`)4    }5}6 7customElements.define('my-button', MyButton)

The method will always tell you which prop and its new and old values.

onAdoption

The onAdoption method is called whenever the component is moved from one document to another. For example, when you move a component from an iframe to the main document.

javascript
1class MyButton extends WebComponent {2    onAdoption() {3        console.log(document)4    }5}6 7customElements.define('my-button', MyButton)

onError

The onError method is called whenever the component fails to perform internal actions. These actions can also be related to code executed inside any lifecycle methods, render, state or style update.

javascript
1class MyButton extends WebComponent {2    onError(error: Error) {3        console.log(document)4    }5}6 7customElements.define('my-button', MyButton)

You may also use this method as a single place to expose and handle all the errors.

javascript
1class MyButton extends WebComponent {2    onClick() {3        execAsyncAction().catch(this.onErrror)4    }5 6    onError(error) {7        // handle error8    }9}10 11customElements.define('my-button', MyButton)

You can also enhance components so that all errors are handled in the same place.

javascript
1// have your global componenent that extends WebComponent2// and that you can use to handle all global related things, for example, error tracking3class Component extends WebComponent {4    onError(error: Error) {5        trackError(error)6        console.error(error)7    }8}9 10class MyButton extends Component {11    onClick() {12        execAsyncAction().catch(this.onErrror)13    }14}15 16customElements.define('my-button', MyButton)

Internals

WebComponent exposes the ElementInternals via the readonly internals property that you can access for accessibility purposes.

To learn about how to create web components that well integrate with forms check the docs on form controls.

javascript
1class TextField extends WebComponent {2    static formAssociated = true // add this to form-related components3    static observedAttributes = ['disabled', 'placeholder']4    disabled = false5    placeholder = ''6 7    render() {8        return html`9            <input10                type="text"11                placeholder="${this.props.placeholder}"12                disabled="${this.props.disabled}"13            />14        `15    }16}17 18const field = new TextField()19 20field.internals // ElementInternals object

Content Root

WebComponent exposes the root of the component via the contentRoot property. If the component has a shadowRoot, it will expose it here regardless of the mode. If not, it will be the component itself.

javascript
1const field = new TextField()2 3field.contentRoot // ShadowRoot object

This is not to be confused with the Node returned by calling the getRootNode() on an element. The getRootNode will return the element context root node, and contentRoot will contain the node where the template was rendered to.

Root

The root tells you about where the component was rendered. It can either be the document itself or the ancestor element shadow root.

edit this doc