Web Component Styles and Functionality
# Web Component Styling
When creating a web component, we start with a template object plus a class which extends HTMLElement. Inside the template we can write any HTML, which includes a <style> element.
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
background-color: #222;
color: #fff;
font-size: 20px;
}
p {
font-size: 2rem;
font-weight: 100;
}
</style>
<div>
<p>Cool Component</p>
</div>
`;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Inside that <style> we can define all the CSS styles that we want to use in the web component.
Any CSS that you would write inside a CSS file loaded into a webpage, you can put inside this style element.
# CSS :host & :host-context()
The :host style allows you to set styles that apply directly to the shadowRoot and therefore your whole component.
The :host-context() method accepts a querySelector-like CSS selector so you can target elements inside your component's shadowRoot and style them.
:host-context(p span) {
/* style spans that are inside of paragraphs in the component */
}
:host-context(.side a) {
/* style anchors that are inside of any element with the class 'side' in the component */
}
2
3
4
5
6
The pseudo-class method provides a higher level of specificity and could help isolating styles in situations where there is a web component nested inside another web component.
# ::slotted()
There is a pseudo-element that can be used inside your web component to target whatever HTML has been injected into your web component to replace the <slot> elements.
::slotted(p) {
/* any paragraph that replaced a <slot> */
}
::slotted(h2) {
/* any h2 that replaced a <slot> */
}
div.footer ::slotted(p) {
/* any <p> that replaced a <slot> if the <slot> was inside <div class="footer"> */
}
#fred::slotted(*) {
/* any element placed into the <slot id="fred"> */
}
2
3
4
5
6
7
8
9
10
11
12
The ::slotted pseudo-element gives us a way to style any content given to the component from the webpage.
MDN reference for ::slotted() (opens new window)
# ::part()
The ::part() pseudo-element is like the opposite of the ::slotted() one. It provides a way for the developer who is creating the web page to style parts from the web component from outside the
shadowRoot.
Inside the web component template we just add part attributes to any element that we want to allow to be styled.
<div>
<h1>Title</h1>
<h2 part="subhead">The first paragraph in the template</h2>
<p part="content">The second paragraph in the template</p>
<p part="content">Another paragraph in the template</p>
</div>
2
3
4
5
6
The two paragraphs above, with the part attributes, can be targeted from the CSS in the web page's CSS file.
cool-component::part(h2) {
/* styling the h2 inside the template */
}
cool-component::part(p) {
/* styling all the p elements inside the template */
}
cool-component::part(p):hover {
/* styling any p element inside the template when it gets hovered over */
}
2
3
4
5
6
7
8
9
MDN reference for ::part() (opens new window)
# Shadow DOM
Each web component will have it's own internal structure that is isolated from the CSS Cascade and JS of the page that is loading them. This internal structure is known as a shadow DOM.
The purpose of the shadow DOM is to allow the web component to define its own styling and control the changes in its own element structure without having to worry about the loading web page doing
anything that will impact it.
When you create a web component, it needs to have a shadowDOM defined with a mode property set to open or closed. The difference being that if you set it to open then the loading web page can
get read-only values from the contents inside the web component.
class CoolComponent extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'closed' });
}
}
2
3
4
5
6
Inside the constructor method we need to call this.attachShadow(). This method creates the container for the shadowDOM for the element (the ShadowRoot). You can think of it as a protected sandbox
for the element to work within. Just like how an iFrame can render another webpage inside a protected space for the current page. The method will return a reference to the shadowRoot that it
creates.
attachShadow method reference (opens new window)
# CSS :host
Inside your element, when you define the internal CSS that the element will use, the CSS selector :host refers to the shadowRoot which contains your web component.
:host {
background-color: rebeccapurple;
color: white;
font-size: 20px;
}
2
3
4
5
You can think of the :host selector for your component, as analogous to the html selector for your webpage.
# HTML Templates
HTML templates can be, and are, used without web components. They simply provide a template for HTML content that you want to use within your JavaScript.
A template can be created inside your HTML file. By default they will not appear on the page.
<template id="mytemplate">
<div>
<h1>Look at Me</h1>
<h2>I am a really cool template</h2>
</div>
</template>
2
3
4
5
6
Or we can create them in JavaScript.
let template = document.createElement('template');
template.innerHTML = `<div>
<h1>Look at Me</h1>
<h2>I am a really cool template</h2>
</div>`;
2
3
4
5
Once you have a template you can use the content property to extract everything that is inside the <template> element.
Once you have the content, you need to generate a copy of it which can be used elsewhere on the webpage or in your web component.
const clone = template.content.cloneNode(true);
//the true value being passed in means copy everything regardless of how many levels deep.
2
In our web component, the variable clone will hold the cloned copy of the template contents. This is what will be our web component. We append this cloned copy to the shadowRoot that we created.
const clone = template.content.cloneNode(true);
shadowRoot.appendChild(clone);
2
Learn more about HTML5 Templates in these videos:
# HTML Slots
Sometimes you will want to pass information from the parent page down into the component. This could be some text content, this could be theming information, or even a function that you want the component to be able to call after an event.
Whatever the reason or the information, the way we can pass it into the component is with the use of <slot> elements.
In your webpage, where you add the new web component tag, you can add HTML content as children of the web component tag. These new HTML elements can have slot attributes. Each slot attribute holds
a name for a matching <slot> element that is inside your component's template.
<body>
<main>
<h1>Page using the Cool Component</h1>
<cool-component>
<div slot="sam">Things to be passed to the web component template.</div>
<div slot="dean">And even more stuff for the template.</div>
</cool-component>
</main>
</body>
2
3
4
5
6
7
8
9
Given the above web component tag and child elements with slot names, and the following template.
let template = document.createElement('template');
template.innerHTML = `<div class="cool">
<h1><slot name="sam">Default h1 content</slot></h1>
<h2><slot name="dean">Default h2 content<slot></h2>
</div>`;
2
3
4
5
The text inside the <slot>s in the template will only appear if elements with matching slot names are not in the web page.
This example would be building content inside the shadowRoot like this:
<div class="cool">
<h1>Things to be passed to the web component template.</h1>
<h2>And even more stuff for the template.</h2>
</div>
2
3
4
The slots have been replaced with the content that was inside the divs with the slot-matching names.
# Web Component Functionality
# Built-in Methods
Inside your web component class, there are a number of built-in methods that you should typically use.
constructor()observedAttributes()connectedCallback()disconnectedCallback()attributeChangedCallback()
let template = document.createElement('template');
template.innerHTML = `
<style></style>
<div class="cool">
<slot name="x"></slot>
</div>`;
export class CoolComponent extends HTMLElement {
someProp = 123; //inside the class you would write this.someProp
static otherProp = 345; //inside or outside the class write CoolComponent.otherProp
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'closed' });
const clone = template.content.cloneNode(true);
shadowRoot.appendChild(clone);
this.element = shadowRoot.querySelector('div.cool');
let slot = this.element.querySelector('slot');
slot.addEventListener('slotchange', function (ev) {
//called anytime content is added or altered
//inside this slot
});
}
static get observedAttributes() {
return ['a'];
//list of supported attributes in <cool-component>
}
get a() {
//keeps property `a` in-sync with attribute <cool-component a="">
//retrieves the value from the attribute when property value is read
return this.getAttribute('a');
}
set a(value) {
//keeps property `a` in-sync with attribute <cool-component a="">
//updates the value in the attribute if property value changes
this.setAttribute('a', value);
}
connectedCallback() {
//method is run when the web component is added to the web page
}
disconnectedCallback() {
//method is run when the web component is removed from the web page
}
attributeChangedCallback(attributeName, oldVal, newVal) {
//method is run when
}
}
window.customElements.define('cool-component', CoolComponent);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
You can use the above script as a template for creating your own web components.
# Properties versus Attributes
When you create an HTMLElement it has attributes and it also has properties. Most of the time the property and the attribute name are the same. But not always. An example of this is the class
attribute. In your HTML you would write <p class="big">. To get the value inside of the class attribute in JavaScript you need to use the className property. In JavaScript, there is also a
property called classList which handles multiple values inside of className as if they were separate values instead of a single string.
We could use p.getAttribute('class') or p.className. The first is looking at the attribute in the HTML. The second is a property on the HTMLElement Object in JavaScript.
If you want to be able to keep a named property value and the attribute value in sync with each other, then we will add a pair of methods - a getter and a setter - for each property.
Using a theme attribute as an example.
static get observedAttributes() {
return ['theme'];
}
get theme(){
return this.getAttribute('theme');
}
set theme(value){
this.setAttribute('theme', value);
}
2
3
4
5
6
7
8
9
Inside the get and set functions you would have the chance to validate or format the value as it is being pulled from or put into the attribute.
All attribute values are strings, regardless of whether it is a number, a boolean or a date value. So, if we wanted the property to hold the numeric version of the string in the attribute then we
could do that conversion inside the get method.
get age(){
//use the unary plus operator to convert the string value in age=""
//to a number if someone asks for the value of the .age property
return +this.getAttribute('age');
}
2
3
4
5
Key Point
HTML attributes are separate from JavaScript properties.
When writing HTML, you are adding attribute to the elements.
In Javascript, when you get a reference to an element, using querySelector() or getElementById() you can use getAttribute() to get the value of the attribute in the HTML.
Some attributes also have a property which will be kept in sync with the attribute by the DOM.
In Web Components, you are responsible for creating the properties and keeping them in sync with the attributes.
# constructor function and slotchange event
The constructor function runs when the class is creating your component. It creates the shadowRoot where everything is loaded. It copies the template and applies the styles. It gives you the
opportunity to create properties that are referencing different parts of the component. Eg: this.element = shadowRoot.querySelector('div.cool').
It also gives you the opportunity to add event listeners for changes to the contents of any slot from your template. there is a slotchange event which will be triggered any time the contents of a
slot-connected element inside your web component is changed in the web page.
<cool-component>
<div slot="a">Stuff in slot A</div>
<div slot="b">Stuff in slot B</div>
<div slot="c">Stuff in slot C</div>
</cool-component>
2
3
4
5
If the code above is what you put in your HTML, and your web component template has the below code:
template.innerHTML = `
<div class="cool">
<h1><slot name="a"></slot></h1>
<h2><slot name="b"></slot></h2>
<p><slot name="c"></slot></p>
</div>
`;
2
3
4
5
6
7
Then we could add a slotchange listener to each slot.
constructor(){
super();
const shadowRoot = this.attachShadow({ mode: 'closed' });
const clone = template.content.cloneNode(true);
shadowRoot.appendChild(clone);
this.element = shadowRoot.querySelector('div.cool');
let slots = this.element.querySelectorAll('slot');
let slotA = slots[0];
let slotB = slots[1];
let slotC = slots[2];
slotA.addEventListener('slotchange', this.someFunc);
slotB.addEventListener('slotchange', this.someFunc);
slotC.addEventListener('slotchange', this.someFunc);
}
someFunc(ev){
//this function will be called whenever the contents of any of the
//HTML slots get changed.
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# AssignedElements and AssignedNodes
When you want to access the content that is passed into a slot, we can do that using two methods - assignedElements() and assignedNodes(). These methods are available on the <slot> elements
inside your template.
Start with a slot in your template, in your web component.
template.innerHTML = `
<div>
<slot name="fred"></slot>
</div>
`;
2
3
4
5
In the web page that holds your component, the web developer can connect to your <slot> and pass in content by adding a slot attribute that matches the name in your template.
<body>
<web-component>
<h2 slot="fred">Some content for the component</h2>
</web-component>
</body>
2
3
4
5
With the matching name it means that the <h2> element will be inserted into the actual web component content and replace the <slot> with the matching name.
In your web component script, in the constructor() function or in the connectedCallback() you can use JavaScript to get a reference to the <slot> element.
let slot = this.root.querySelector('slot');
slot.assignedElements();
slot.assignedNodes();
2
3
The assignedElements() method will return an array of all the top level elements that are being passed into the slot. In our case, it is just the <h2> element.
The assignedNodes() method will return an array of elements and textnodes.
Once you have references to the content that was pushed into the <slot> in your component you can treat it like any other HTML and do what you want with it.
# observedAttributes
The observedAttributes method, inside the component class, needs to return an Array. The array contains a list of attribute names that you want your component to support. These are attributes that are
specific to your component and that are beyond the global ones supported by any element (like id or title).
As an example, if your component were going to use an attribute theme and another called type, then the method would look like:
static get observedAttributes() {
return ['theme', 'type'];
}
2
3
This method is a static method. It belongs to the class, not the instance. Remember to add the keyword static in front of get.
# connectedCallback and disconnectedCallback methods
These two methods will run when your component is added to the DOM or removed from the DOM. If the element was part of the HTML when the page is loaded then the connectedCallback method would be
called as the HTML is parsed.
The most obvious question you would ask is why would you want to put code inside of the connectedCallback method.
Since these web components are dynamic and can include any functionality that we want, that means that the content could be dynamically generated or be coming from the Cache, LocalStorage, or even an API.
The constructor function creates the initial shadowRoot and all the HTML tags that replace the one written in the web page. It does the structure and styling.
The connectedCallback lets you add content, modify content, or add event listeners.
If there is any clean up required, like removing event listeners or saving current state, when the element is removed from the page, then that can be done in the disconnectedCallback method.
# attributeChangedCallback method
Any time the value of an attribute is changed in the HTML, for example <cool-component theme="big"> becomes <cool-component theme="little">, or when the value is first set during the
connectedCallback, then attributeChangedCallback gets called.
This method will be passed three things - the name of the attribute, the old value of the attribute and the new value of the attribute - in that order.
Typically, you would use a switch statement to look at the name of the attribute and then have specific code based on which attribute value was changed or initially set.
# Events in Components
If you wish to add event listeners to any part of your web component then you should do this inside of your connectedCallback function. This is the point where the element has been added to the DOM.
In the disconnectedCallback function you should use the removeEventListener method to dispose of the listeners and free the memory used in maintaining them.
Using this HTML as the example content of a web component.
<div class="dialog">
<h1><slot name="title">Title</slot></h1>
<p><slot name="message">Message</slot></p>
<div>
<button class="btnYes">Approve</button>
<button class="btnNo">Reject</button>
</div>
</div>
2
3
4
5
6
7
8
Then inside the connectedCallback function we would add our listener functions.
class Agreement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'closed' });
const clone = template.content.cloneNode(true);
shadowRoot.appendChild(clone);
this.element = shadowRoot.querySelector('div.dialog');
this.btnYes = this.element.querySelector('button.btnYes');
this.btnNo = this.element.querySelector('button.btnNo');
}
connectedCallback() {
this.btnYes.addEventListener('click', (ev) => {
// ev.target is the button
// avoid using `this` inside here
console.log('Yes button clicked');
});
this.btnNo.addEventListener('click', (ev) => {
// ev.target is the button
// avoid using `this` inside here
console.log('No button clicked');
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
When you are building a component, you are trying to create something that can stand alone. It can be imported into any website. Any website will only need to import the one JS file and it will have all the built in functionality.
Let's say that you have a button element inside your component. Inside your class you can create a method that will be called when a user clicks the button.
This method is typically a private method. By making it private it cannot be called directly outside of the class.
let template = document.createElement('template');
template.innerHTML = `<div>
<h1>A Title</h1>
<button>Click Me</button>
</div>`;
class CoolComponent extends HTMLElement {
constructor() {
super();
const clone = template.content.cloneNode(true);
shadowRoot.appendChild(clone);
this.btn = shadowRoot.querySelector('button');
}
connectedCallback() {
btn.addEventListener('click', this.#handleClick);
}
#handleClick(ev) {
console.log('the button inside the component was clicked');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
The developer who uses the component will be able to add event listeners to the outer container. If the component creates an element called <my-component>, then the website developer could add an
event like this:
let mc = document.querySelector('my-component');
mc.addEventListener('click', function (ev) {
//ev.target will be a reference to the <my-component> element
//any time that a user clicks anywhere in the <my-component> this function can run.
});
2
3
4
5
The developer who imports the component adds event listeners to the outer container. Events inside the component are able to bubble up and out of the component. So, in the parent script, which
belongs to the website, you can listen for these bubbling events.
To do this, we need to initiate an event inside the component. The event can pass information up from the component to the webpage, using a CustomEvent object.
MDN CustomEvent constructor reference (opens new window).
class CoolComponent extends HTMLElement {
count = 0;
constructor() {
super();
const clone = template.content.cloneNode(true);
shadowRoot.appendChild(clone);
this.btn = shadowRoot.querySelector('button');
this.heading = shadowRoot.querySelector('h1');
}
connectedCallback() {
btn.addEventListener('click', this.#handleClick.bind(this));
//use `bind` so the #handleClick function will treat `this` as a reference to
//the component instead of a reference to the button.
}
#handleClick(ev) {
console.log('the button inside the component was clicked');
this.count++; //increment the count variable
let happyEvent = new CustomEvent('happy', {
detail: {
name: "we can send anything we need",
count: count,
});
//happyEvent is an event that we want to send to the webpage.
//the event "type" is "happy"
this.heading.dispatchEvent(happyEvent);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Here is the reference to the dispatchEvent function (opens new window).
With the happy event being dispatched from inside the component, the website developer can add a listener for this event.
let mc = document.querySelector('my-component');
mc.addEventListener('happy', function (ev) {
//this will happen when the happy event bubbles up from inside the component
let params = ev.detail;
console.log(params.name);
console.log(params.count); //the count instance member value from inside the component
});
2
3
4
5
6
7
Back inside the component, if you want to stop the click event from bubbling up from the button to the parent webpage, then you can use the stopPropagation method inside that #handleClick
method.
#handleClick(ev) {
console.log('the button inside the component was clicked');
ev.stopPropagation();
//this stops the click event from bubbling up to the parent webpage.
}
2
3
4
5
Here is a more detailed tutorial on working with Custom Events inside Web Components.
# Passing Functions to Components
Now, while we can frequently have functions connected to event listeners, like the ones above, and all the behaviour that we need can be embedded inside those functions. There will be times when we want to be able to provide a function from our own code and have an event inside the web component call that function.
There are a couple ways for us to achieve this.
First, in our own script we can add a listener to the web component element itself. This will work for the container of the component but not for elements that are inside of the shadowRoot.
Second, and more effectively, we can pass the name of a function, as a string through an attribute in the HTML of our web component.
<script>
function f1() {
//an example function inside our own script, not the web component script
console.log('Function f1 inside our own script.');
}
</script>
<cool-component func="f1"></cool-component>
2
3
4
5
6
7
So, effectively what we are doing is passing the name of the function into the component's script.
Inside our component script, in the attributeChangedCallback function, we can access that name.
Next, we use the square bracket notation to access the function. In the same way that window['document'] gives us access to the document object inside the global window object, we could use
window['f1'] to access our f1 function and window['f1']() to run the f1 function.
attributeChangedCallback(attributeName, oldVal, newVal) {
switch(attributeName){
case 'func':
//when an attribute called func is given a value
if( newVal in window){
//check and see if the window object has a property called `f1`
if( typeof window[newVal] === 'function'){
//make sure that the property is a function
//save the function in a property that belongs to our component
this.firstFunction = window[newVal];
}
}
break;
default:
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Once we have created and assigned the function to this.firstFunction we can then use it anywhere within our component, including in those click event listeners we created above.
There are a couple of issues though.
First, the keyword this is not going to work inside the click listener function as we have defined it. To get around this, we can assign our function to a variable that is local to the
connectedCallback function. This can be used inside the listener without dealing with this.
connectedCallback() {
const func = this.firstFunction;
this.btnYes.addEventListener('click', (ev) => {
// ev.target is the button
// avoid using `this` inside here
console.log('Yes button clicked');
func();
});
}
2
3
4
5
6
7
8
9
An alternate approach to the above solution is to bind the event listener function to our component like the following:
connectedCallback() {
//at this point in the code, `this` refers to the component.
// this.firstFunction is the property we created in the attributeChangedCallback method
this.btnYes.addEventListener('click', this.handleYesButton.bind(this) );
//call `bind()` on the handleYesButton function so it will use the same this as here
}
handleYesButton(ev){
// ev.target is the button
// and now `this` does refer to the component
console.log('Yes button clicked');
this.firstFunction();
//will work here now
}
2
3
4
5
6
7
8
9
10
11
12
13
14
The second problem goes back to the if( typeof window[newVal] === 'function'){ line, where we were extracting the name of the function from attribute. This approach to validating a function works
well if we have a normal script that is NOT a module. In modules, functions are NOT added to the window object when you declare them.
So, to test if a function exists from a module we need to use the eval() method.
if (typeof eval(newVal) === 'function') {
//newVal is a function inside the current or imported module.
}
2
3
Notice the how the comment in the above code sample says current or imported module. That is important. With the eval() method we are actually checking to see if there is actually some function
with that name that we can call within the current context.
If the function that we want to call is in the main page, then we can't call the function from inside the component.
We actually need to bubble up an event from inside the component to the parent page with instructions to call the function. When we bubble up the instructions, it will again be the string name of the function, not a reference to the function itself. If you are calling a function whose name has been passed into a component then the function needs to exist inside the component module.
# Approaches and Reasons for Passing Functions
So, that solves the how do you can turn string names into function references. But this is not WHAT we would typically do.
Think about the process of creating a component to use on a website.
- Developer A designs and builds a component.
- Developer A writes the documentation telling other developers how to use the component.
- In that documentation they will list the internal public functions which can be called from the webpage.
- In that documentation they will list the CSS variables that can be used in the parent page to set values for the CSS inside the shadow DOM.
- Developer A will list events that will bubble up from inside (if any).
- Developer A is NOT going to list private functions inside the component because they can't be called from outside the component.
So, why would there be an attribute that can hold a function name?
Components don't want to know the name of functions in the web page.
However, Developer B, who is building the webpage might want to know when a button inside the component is clicked. And if that click doesn't bubble up (not all events do), Developer B might still want to use that button click to make one of their own functions run.
So, Developer B, adds the name of one of their own functions to a component attribute as a string. Now the component can trigger a custom event internally. That custom event can contain a detail
object which will deliver the name of the function back to the parent webpage. The parent webpage can extract the name of the function from the custom event detail and convert the string into an
actual function reference to be called.
Here is an example. The parent page has a component with a func attribute. The value is the name of a function inside the main.js script, attached to the webpage.
<!-- main web page -->
<html>
<head>
<script type="module" src="./main.js"></script>
</head>
<body>
<p>The button in the component has been clicked <span>0</span> times.</p>
<p>
<my-component func="updateCount"></my-component>
</p>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
// main.js
(() => {
//IIFE that runs after DOMContentLoaded
let component = document.querySelector('my-component');
component.addEventListener('applejuice', handleCustomEvent);
})();
function handleCustomEvent(ev) {
//when a custom `applejuice` event bubbles out of the component
let detail = ev.detail;
let type = ev.type; //applejuice
let func = detail.funcName;
if (typeof eval(func) === 'function' && type === 'applejuice') {
//call the updateCount function
eval(func)();
}
}
function updateCount() {
let span = document.querySelector('body p span');
let count = Number(span.textContent);
span.textContent = (count++).toString();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Inside the component we just have to add a click listener to the button, as shown in the event section above. Then inside the component private click listener function, we create a custom event with
an applejuice type, and then dispatch it.
//inside the component click listener function
#handleClick(ev){
ev.stopPropagation();
//do whatever internal things we need first
let f = this.functionNameFromAttribute;
let event = new CustomEvent('applejuice', {detail: {func:f}});
this.button.dispatchEvent(event);
}
// assuming that a reference to the button is held in this.button
// assuming that a reference to the function name in the attribute is held in this.functionNameFromAttribute
// the custom applejuice event will bubble up to the webpage, out through the shadowRoot boundary
2
3
4
5
6
7
8
9
10
11
# Additional Component Behaviours
If you want to add additional functions to your class we can add either instance methods or static class methods.
class Thing extends HTMLElement {
static numThings = 0;
id;
constructor() {
super();
//...
Thing.numThings = Thing.numThings + 1;
this.id = Thing.numThings;
}
doInstanceSomething() {
//an instance method
console.log(`My id is ${this.id}`);
}
static doClassSomething() {
//a class method.
console.log(`The total number of Things is ${Thing.numThings}`);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Instance methods are methods that are attached to each object created by a class. The method knows which object they belong to. To call an instance method you put the name of the instance in front of the method name.
let obj1 = new Thing();
let obj2 = new Thing();
let obj3 = new Thing();
obj1.doInstanceSomething(); // obj1.id is 1
obj2.doInstanceSomething(); // obj2.id is 2
obj3.doInstanceSomething(); // obj3.id is 3
2
3
4
5
6
Class methods are declared with the word static in front of them. They belong to the class itself, not to any instance. The value of any static class variable is shared between all the instances. To
access one of them, we put the name of the class in front of the property or method name.
let obj1 = new Thing(); // Thing.numThings is 1
Thing.doClassSomething(); // The total number of Things is 1
let obj2 = new Thing(); // Thing.numThings is 2
Thing.doClassSomething(); // The total number of Things is 2
let obj3 = new Thing(); // Thing.numThings is 3
Thing.doClassSomething(); // The total number of Things is 3
2
3
4
5
6
If you want to learn more about JavaScript class syntax, including static properties and private properties, watch this video:
# Creating Components via JavaScript
In your web page, if you want to create and append a web component without initially having it in the HTML, then you can create it the same way you would any other chunk of HTML.
let component = document.createElement('cool-component');
//which will trigger the component constructor
let div = document.createElement('div');
div.setAttribute('slot', 'slotname');
div.textContent = 'The text that will be passed into <slot name="slotname">';
component.append(div);
//then add it to the page
document.querySelector('main').append(component);
//which will trigger the connectedCallback method
2
3
4
5
6
7
8
9
# Free eBook reference for Web Components
I recently found this free eBook that you can also use as a reference for Web Components. There is also a link to the paid course on FrontEndMasters.com but you don't have to take the course to use the book.
Dave Rupert's eBook on Web Components (opens new window)
# Using Icons in Components
So, you have a great idea for a web component but you want to add custom icons inside the component.
The idea behind great web components is to be able to share a single file or single link with other developers which contain ALL the functionality and ALL the styling. We don't want users to have to download multiple files, add extra links to their web pages, and have to place the links in the correct files with the proper relative paths. That just creates a barrier to using your component.
So, if the user isn't going to download extra files beyond the one JS file, then how do you include icons in your component?
The answer is SVG.
You can add an <img> element inside your component that points to an external resource url which loads an SVG file. However, if that URL ever fails, there will be no icon in the component. OR if the
user goes offline, again there will be no icon in the component.
So, we need to embed the SVG inside the component. The first way is to actually add the SVG directly inside the HTML.
<p>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 200 200">
<rect class="nose" width="100" height="100" rx="50" ry="50" fill="#ff0000" stroke="#000" />
</svg>
</p>
2
3
4
5
You can add an SVG element directly inside the HTML which is in your template that defines your component content. By using this approach you can use CSS to style the fills and strokes inside the SVG element. Think of the possibilities of using CSS with CSS properties (variables) that use the website branding colors to style the SVG element.
With the SVG in the HTML you can also use CSS to add styles for pseudo-classes like :hover and :active.
If you have no hover effects and want to style all the colors for the SVG directly inside of the SVG, you can also set an embedded SVG element as the data: URI for a background-image in your CSS.
span.mysvg {
display: inline-block;
width: 50px;
height: 50px;
background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%20150%20150%22%20shape-rendering%3D%22geometricPrecision%22%20text-rendering%3D%22geometricPrecision%22%3E%0A%20%20%20%3Cpath%20d%3D%22M148.53438%2C177.20732c-9.34191%2C0-18.22272-1.97419-26.248-5.52812.687664-10.448517-7.501265-26.062374-13.281988-40.084948L84.74803%2C124.28495c-.72288-3.87797-1.10084-7.87726-1.10084-11.96483c0-35.83621%2C29.05098-64.88719%2C64.88719-64.88719s64.88719%2C29.05098%2C64.88719%2C64.88719c0%2C4.08757-.37796%2C8.08686-1.10084%2C11.96483l-23.117021%2C7.309302c-4.558202%2C6.668397-9.795934%2C22.79906-13.289519%2C39.570628-8.31888%2C3.87727-17.5965%2C6.04243-27.37981%2C6.04243v.00001Z%22%20transform%3D%22matrix%281.079663%200%200%201.079663-85.274786-45.668803%29%22%20fill%3D%22%23fff%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%2F%3E%0A%20%20%20%3Cellipse%20class%3D%22eyes%22%20rx%3D%2215%22%20ry%3D%2215%22%20transform%3D%22matrix%281.079663%200%200%201.079663%20100.940569%2060.010166%29%22%2F%3E%0A%20%20%20%3Cellipse%20class%3D%22eyes%22%20rx%3D%2215%22%20ry%3D%2215%22%20transform%3D%22matrix%281.079663%200%200%201.079663%2049.243998%2060.010166%29%22%2F%3E%0A%20%20%20%3Crect%20class%3D%22nose%22%20width%3D%225%22%20height%3D%2220%22%20rx%3D%220%22%20ry%3D%220%22%20transform%3D%22matrix%281.079663%200%200%201.079663%2076.345624%2074.801956%29%22%20stroke-width%3D%220%22%2F%3E%0A%20%20%20%3Crect%20class%3D%22nose%22%20width%3D%225%22%20height%3D%2220%22%20rx%3D%220%22%20ry%3D%220%22%20transform%3D%22matrix%281.079663%200%200%201.079663%2069.55046%2074.801956%29%22%20stroke-width%3D%220%22%2F%3E%0A%20%20%20%3Cpath%20d%3D%22M181.537773%2C150c-20.890965%2C4.507309-44.21305%2C4.96703-64.622002%2C0c15.543043%2C4.349274%2C47.854041%2C3.940847%2C64.621995%2C0h.000007Z%22%20transform%3D%22matrix%281.111375%200%200%201.079663-90.148342-48.851999%29%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%2F%3E%0A%20%20%20%3C%2Fsvg%3E');
}
2
3
4
5
6
Just remember that you will need to URL Encode the SVG element before adding it to the CSS in your component HTML template. The above example uses an embedded SVG image as the data uri background
image for a span with the CSS class name mysvg.
Here is an online tool (opens new window) that you can use to convert an SVG to a URLEncoded version of the SVG element to use in your CSS.