Bovenkant pagina

JavaScript frameworks, meet Web Components

Blog

Experimenting with Web Components

Web Component using Vanilla JS

Building a native web component (without any framework) is really fun to do! Since you are writing plain JavaScript, it feels like a mix of old school event handlers with new school ES6 goodness. It is also quite educational as you learn exactly how a web component works. But frameworks exists for a reason, it's very labor intensive and difficult to transpile back to ES5.

Source code (simplified):

class HueSlider extends HTMLElement {
  constructor() {
    super()
    const shadowRoot = this.attachShadow({ mode: 'open' })
    shadowRoot.innerHTML = `
     <label>
        <input type="range" min="0" max="360">
        <output><slot></slot></output>
      </label>
    `
  }
  static get observedAttributes() { return ['hue'] }
  attributeChangedCallback() {  /* ... */ }
  handleInput (event) {
    event.stopPropagation()
    this.dispatchEvent(new CustomEvent('hueChange', this.input.value))
  }
}

Polymer / LitElement

The Polymer Project created LitElement "A simple base class for creating fast, lightweight web components". Together with the polymer-cli this creates a powerful combination. LitElement is a small base class which gives you the close-to-the-metal feeling but with a reactive renderer.

Source code (simplified):

class HueSlider extends LitElement {
  static get properties() {
    return { hue: { type: String } }
  }
  constructor() {  /* ... */ }
  handleChange(event) {
    /* ... */
    this.dispatchEvent(new CustomEvent('hueChange', this.hue)) 
  }
  render() {
    return html`
      <label>
        <input type="range" min="0" max="360" value=${this.hue} @change=${this.handleChange} />
        <output style="background-color: hsl(${this.hue}, 100%, 50%);">
          <slot></slot>
        </output>
      </label>
    `
  }
}

React

For the React version of our component we used Create React App to quickly get going. We had to use the react-web-component package to wrap our React component into a web component. A side-effect of using React for building web components is that it is included in the resulting output bundle and requires you to have React as an external dependency on the page.

Source code (simplified):

export default class HueSlider extends React.Component {
  constructor(props) { /* ... */ }
  handleChange = () => { /* ... */ }
  webComponentConstructed() { /* ... */ }
  render() {
    const { hue } = this.state
    return (
     <label>
        <input type="range" min="0" max="360" value={hue} onChange={this.handleChange} />
        <output style={{ backgroundColor: `hsl(${hue}, 100%, 50%)` }}>
          { this.props.children }
        </output>
      </label>
    )
  }
}

SkateJS

SkateJS positions itself as “a functional abstraction over the web component standards”. It’s close-to-the-metal but some things are being taken care of or are abstracted away. One of the options SkateJS offers is what renderer you would like to use. Also, SkateJS isn’t that well known yet, so there was only little documentation and it wasn’t very extensive.

Source code (simplified):

class HueSlider extends withComponent(withPreact()) {
  static get props() {
    return {
      hue: props.string
    }
  }
  connectedCallback() { ... }
  handleChange = () => { ... }
  render({ hue, handleChange }) {
    return (
      <label>
        <input type="range" min="0" max="360" value={hue} onInput={handleChange} />
        <output style={{ backgroundColor: `hsl(${hue}, 100%, 50%)` }}>
          <slot></slot>
        </output>
      </label>
    )
  }
}

Stencil

Stencil is a compiler that generates web components. It mixes TypeScript classes with JSX to produce web components. We did not use Stencil before so we needed a bit of getting used to the syntax and build setup.

Source code (simplified):

@Component({
  tag: 'hue-slider-stencil',
  styleUrl: 'hue-slider-stencil.css',
  shadow: true
})

export class HueSlider {
  @Prop({ reflectToAttr: true, mutable: true }) hue: string = '100'
  @State() inputValue: string
  @State() value: string
  @Event({ eventName: 'input'}) inputEvent: EventEmitter
  handleInput() { ... }
  componentWillLoad() { ... }
  @Watch('hue')
  hueUpdated(val) { ... }
  render() {
    return (
      <label>
        <input type="range" min="0" max="360" value={`${this.hue}`} onInput={(e) => this.handleInput(e)} />
        <output style={{ backgroundColor: this.value }}>
          <slot></slot>
        </output>
      </label>
    )
  }
}

Svelte

Although we have used Svelte before, we tend to use Vue more often for our day-to-day work. However, since they share a quite similar API, working with Svelte is much like working with Vue.

The compiler of Svelte can convert components directly to web components. Having web component support baked into the library.

Source code (simplified):

<label on:input="onInput(e)" on:change="onChange(e)">
  {#if hue}
    <input type="range" min="0" max="360" value="{inputValue}">
  {:else}
    <input type="range" min="0" max="360">
  {/if}
  <output style="background-color: hsl({inputValue}, 100%, 50%)">
    <slot></slot>
  </output>
</label>
<script>
  export default {
    tag: 'hue-slider-svelte',
    data() {
      return {
        hue: '0',
        inputValue: '0',
        value: '0'
      }
    },
    onstate() { ... },
    onupdate() { ... },
    oncreate() { ... },
    methods: {
      onInput() { ... },
      onChange() { ... }
    }
  }
</script>

Vue.js

We use Vue.js quite often here at De Voorhoede. So we are comfortable with its syntax and build system. The creation of the component took little effort. To transform the Vue component into a web component, we used the @vue/web-component-wrapper library to wrap our Vue component into a web component. This approach requires you to have Vue as an external dependency on the page.

Source code (simplified):

<template>
  <label>
    <input type="range" min="0" max="360" :value="inputValue" @input.stop="onInput" @change.stop="onChange">
    <output :style="`background-color: hsl(${inputValue}, 100%, 50%)`">
      <slot />
    </output>
  </label>
</template>
<script>
  export default {
    name: 'HueSlider',
    props: ['hue'],
    data: () => ({
      inputValue: null,
      value: '0'
    }),
    methods: {
      setValue() { ... },
      onInput() { ... },
      onChange() { ... }
    },
    mounted() { ... },
    watch: { ... }
  }
</script>

Using the generated web components

It’s actually quite easy to use web components different frameworks. It mostly boiled down to importing the component and defining it on the page. We could use the web components in Storybook and use some web components in Framer X, an interactive design tool (still in beta).

This means that we can develop components and designers can use these as interactive building blocks within their Framer X project. Very cool stuff!

← Alle blogposts

Ook verliefd op het web?

Technologie en gebruikservaring. Snel, toegankelijk en een plezier om te gebruiken. Maar ook een plezier om te ontwikkelen. Geldt dit ook voor jou?
Kom ons versterken