-
Notifications
You must be signed in to change notification settings - Fork 11
Creating a web component with SlingElement and html
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.
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.
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.
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.
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) };
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));