Skip to content

Creating a web component with SlingElement and html

Leonardo Favre edited this page Oct 19, 2018 · 5 revisions

Introduction

This tutorial will guide you through the steps of creating a web component with SlingElement.

We will build a Star Rating component that contains five clickable stars which the user can interact with to rate something. The rate can also be passed through an html attribute or property. The component throws DOM events that the application can observe to react to rate changes.

Development

const StarRating = (Base = class {}) => class extends Base {};

We start by creating a decorator that later will receive the SlingElement class.

const StarRating = (Base = class {}) => class extends Base {
  constructor() {
    super();
    this.rate = 0;
  }
};

In the constructor, we set initial values to the properties that the component will need. In our case, we set rate to zero.

Declaring properties

const StarRating = (Base = class {}) => class extends Base {
  // omitted code

  static get properties() {
    return {
      rate: {
        type: Number, // could be String, Boolean, Object or Array
        reflectToAttribute: true, // can be set through html
        observer: 'restrictRate', // calls this method when rate changes
      },
    };
  }
};

We now tell the component how to handle properties by declaring a static getter called properties. We define that the rate property is a Number and that it can be passed to the component through an html attribute.

If set to false, reflectToAttribute would cause rate to only be passable through javascript. This is useful when dealing with complex values like objects or arrays, which shouldn't be passed through html.

<!-- Passing rate through html -->
<star-rating rate="4"></star-rating>
// Passing rate through javascript
const starRatingElement = document.querySelector('star-rating');
starRatingElement.rate = 4;

With observer: 'restrictRate', we tell the component to call the restrictRate method every time rate changes. The method receives the current and the last property values as first and second parameters, respectively.

const StarRating = (Base = class {}) => class extends Base {
  // omitted code

  restrictRate(newRate, oldRate) {
    if (Math.round(newRate) !== newRate || newRate < 0 || newRate > 5) {
      this.rate = Math.round(Math.max(0, Math.min(5, newRate)));
    }
  }
};

The restrictRate method coerces rate to an integer between zero and five.

Rendering html

import { html } from 'sling-framework';

const StarRatingView = ({ rate, handleStarClick }) => html`
  <style>
    button { color: grey; border: none; font-size: 32px; }
    button.selected { color: gold; }
  </style>
  ${[1, 2, 3, 4, 5].map(index => html`
    <button
      className="${index <= rate ? ' selected' : ''}"
      onclick=${handleStarClick(index)}></button>
  `)}
`;

To render html, we create StarRatingView, a function that receives arguments passed by the component and returns html. The function uses the html helper provided by Sling Framework.

StarRatingView draws five stars that are colored according to the current rate value.

const StarRating = (Base = class {}) => class extends Base {

  // omitted code

  handleStarClick(index) {
    return () => {
      this.rate = index;
    };
  }
};

We also define that the handleStarClick method will be called every time a star is clicked, so that we can react to user interaction.

const StarRating = (Base = class {}) => class extends Base {
  constructor() {
    super();
    this.rate = 0;
    this.handleStarClick = this.handleStarClick.bind(this);
  }

  // omitted code

  render() {
    return StarRatingView(this);
  }
};

The StarRatingView is called by the render method in the component, which is always called when a declared property changes.

We have to bind this to handleStarClick in the constructor or it will point to the button instead of the parent component.

Dispatching events

At this point, the component is working as expected, but the application is not aware of what's happening inside of it. To work this out, we dispatch custom DOM events that can be observed by the application. SlingElement implements a method called dispatchEventAndMethod that does that.

const StarRating = (Base = class {}) => class extends Base {
  // omitted code

  static get properties() {
    return {
      rate: {
        type: Number,
        reflectToAttribute: true,
        observer(newRate, oldValue) {
          this.restrictRate(newRate);
          this.dispatchEventAndMethod('rate', this.rate);
        },
      },
    };
  }
};

Note that the observer key was changed to accept a method instead of a string. The result is the same: when the rate property changes, the observer method is called receiving the current and the old property values.

At this point, it is be possible to listen for the rate event an implement the onrate method at the application, like this:

document.addEventListener('rate', evt => { console.log(evt.detail) });
const starRatingElement = document.querySelector('star-rating');
starRatingElement.onrate = evt => { console.log(evt.detail) };

Complete example

That's it. We have finished the Star Rating component. Here's the complete code:

import { SlingElement, html } from 'sling-framework';

const StarRatingView = ({ rate, handleStarClick }) => html`
  <style>
    button { color: grey; border: none; font-size: 32px; }
    button.selected { color: gold; }
  </style>
  ${[1, 2, 3, 4, 5].map(index => html`
    <button
      className="${index <= rate ? ' selected' : ''}"
      onclick=${handleStarClick(index)}></button>
  `)}
`;

const StarRating = (Base = class {}) => class extends Base {
  constructor() {
    super();
    this.rate = 0;
    this.handleStarClick = this.handleStarClick.bind(this);
  }

  static get properties() {
    return {
      rate: {
        type: Number,
        reflectToAttribute: true,
        observer(newRate) {
          this.restrictRate(newRate);
          this.dispatchEventAndMethod('rate', this.rate);
        },
      },
    };
  }

  restrictRate(newRate, oldRate) {
    if (Math.round(newRate) !== newRate || newRate < 0 || newRate > 5) {
      this.rate = Math.round(Math.max(0, Math.min(5, newRate)));
    }
  }

  handleStarClick(index) {
    return () => {
      this.rate = index;
    };
  }

  render() {
    return StarRatingView(this);
  }
};

window.customElements.define('star-rating', StarRating(SlingElement));