Let's create an Element
Let's create an Element. A basic one. One that can translate the word "World" into 3 languages: Spanish, French and Russia.
At the end of the day it will look like this:
Hello <my-world lang="russian"></my-world>
First let's get the translation logic ready:
const WORLD_DICT = {
"spanish": "mundo",
"french": "monde",
"russian": "mir"
};
Using this simple object I can get the right translation for "World" by retrieving the value for a key named after the language I'm after.
WORLD_DICT["spanish"]
will yield the valuemundo
WORLD_DICT["french"]
will returnmonde
Ok! Now let's display the translated word. To do that we'll need a hosting element. A <span>
would fit perfectly!
const spanEl = document.createElement('span');
Put it in our document's body:
document.querySelector('body').appendChild(spanEl);
Now that we have the span ready and placed inside
, let's populate it with our translated "World" for Spanish (not to worry! we will later extend the language selection 🙂).spanEl.textContents = WORLD_DICT["spanish"];
If we'd combine all the code snippets above, we'd get a nice span with the word "mundo" inside it placed in our <body>
tag.
Now let's move on to creating a real HTML element that can translate to all three languages!
Creating our custom element class
In order to create a custom HTML element, two steps are required:
- Create a Class that extends HTMLElement (don't worry if you're unfamiliar with these concepts). It is called a custom element class.
- Register that class using the
window.customElement.define();
method.
For the first task, I will use Element-F, a library that helps simplify the creation of custom elements.
Let's begin by passing ElementF two arguments:
- A creation function. This function will be called whenever a new instance of our custom element
<my-world>
is used, and will decide the way it will look and behave. - The names of the attributes (passed as strings in an array) that we'd like to offer to our element's users. ElementF will let us know whenever any of them changes. In our case, we'd only need the attribute name "lang", with which our users could set a language.
const myWorldClass = ElementF(
creationFunction, // This is the creation function (we'll deal with it next
["lang"] // That's a list of attributes that we wish to be available to our element's users.
);
Let's define our creationFunction
. First, I'll just copy all our code from above inside it:
const creationFunction = function(){
const WORLD_DICT = {
"spanish": "mundo",
"french": "monde",
"russian": "mir"
};
const spanEl = document.createElement('span');
document.querySelector('body').appendChild(spanEl);
spanEl.textContents = WORLD_DICT["spanish"];
}
When you're writing a creation function for ElementF, use only the long-form function definition (
function(){}
) instead of arrow functions (()=>...
). We'll soon find out why.
This, believe it or not, is already a great leap towards our goal!
There are, of course, some changes in order.
First, we wish to host our <span>
within our own element, not the <body>
tag. So:
document.querySelector('body').appendChild(spanEl);
changes into:
this.appendChild(spanEl);
this
inside our creation function references our actual element. So all I needed to do is append our span to it.
Because our creation function provides us with
this
, we cannot use the arrow-style function definition syntax (()=>{}
). Using arrow function will causethis
to be whatever its definition context is.
Second step would be to listen to changes to "lang" and respond by displaying the correct translation for it.
For this, we'd need to use something that ElementF passes to our creation function - a "life" event emitter.
Listening to life events using life is pretty straight forward:
life.on([eventName], [handler]);
ElementF provides a handful of events to respond to. You can find the complete list here.
Now, instead of just populating our <span>
only once, and only with the translation to Spanish, let's keep updating it whenever "lang" attribute changes.
const creationFunction = function(life){
const WORLD_DICT = {
"spanish": "mundo",
"french": "monde",
"russian": "mir"
};
const spanEl = document.createElement('span');
life.on('attribute:change', ({ newValue })=> spanEl.textContents = WORLD_DICT[newValue]);
life.once('connect', ()=> this.appendChild(spanEl));
}
Now we're listening to an event called attribute:change
. Whenever it occurs, our handler function will be called, and as an argument it will be passed an object with newValue
and previousValue
fields. newValue
is the freshly set value, which is all we need.
If you've been paying close attention, you probably noticed another change I've slipped in. I listen to an event called connect
, which happens anytime my element is added to a document.
A clarification is in order 😇:
- I've listened to the
connect
event withonce
method.once
is just likeon
, but will only invoke my handler once. - I'm required to wait for the
connect
event before I can interact with the element's DOM (to append<span>
) because of how custom elements are designed (see spec).
Let's use it!
At this stage we have a custom element class ready, but the DOM will not recognize it yet. This line will register it for us as my-world
tag:
window.customElements.define('my-world', myWorldClass);
That's it! Now let's try our snippet from above:
Find out more
- To fiddle around with ElementF, you can use its UMD version available through unpkg.
- ElementF repo.
Happy coding 😉
Top comments (0)