Categories
Angular Web Components

Creating Web Components with Angular

Angular is a popular front-end framework made by Google. Like other popular front-end frameworks, it uses a component-based architecture to structure apps.

In this article, we’ll look at how to create Angular Elements, which are packaged as Web Components.

Angular Elements

Web Components are supported by most browsers like Chrome, Firefox, Opera or Safari.

We can transform Angular components to Web Components to make all the Angular infrastructure available to the browser.

Features like data-binding and other Angular functionalities are mapped to their HTML equivalents.

Creating and Using Custom Elements

We can create a Web Component by creating an Angular component, then building it into a Web Component.

To create a Web Component with Angular, we have to do a few things.

First, we create a component to build into Web Components. Then we have to set the component we created as the entry point.

Then we can add it to the DOM.

We’ll make a custom component to get a joke. To do this, we first run:

ng g component customJoke
ng g service joke

to create our component and service to get our joke and display it.

Then we run:

ng add @angular/element

to add the Angular Element files to create our Web Component.

Then injoke.service.ts , we add:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class JokeService {

  constructor() { }

  async getJokeById(id: number) {
    const response = await fetch(`http://api.icndb.com/jokes/${id}`)
    const joke = await response.json();
    return joke;
  }
}

The code above gets a joke from the Chuck Norris API by ID.

Next, we write our component code as follows:

app.module.ts :

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { AppComponent } from './app.component';
import { CustomJokeComponent } from './custom-joke/custom-joke.component';
import { JokeService } from './joke.service';

@NgModule({
  declarations: [
    CustomJokeComponent,
    AppComponent
  ],
  imports: [
    BrowserModule,
  ],
  providers: [JokeService],
  bootstrap: [AppComponent],
  entryComponents: [CustomJokeComponent]
})
export class AppModule {

}

In AppModule , we add CustomJokeComponent to entryComponents so that it’ll be the entry point component instead of AppComponent .

It’ll load when custom-joke element is created.

app.component.ts :

import { Component, Injector } from '@angular/core';
import { createCustomElement, WithProperties, NgElement } from '@angular/elements';
import { CustomJokeComponent } from './custom-joke/custom-joke.component';

@Component({
  selector: 'app-root',
  template: ''
})
export class AppComponent {
  constructor(injector: Injector) {
    const JokeElement = createCustomElement(CustomJokeComponent, { injector });
    customElements.define('custom-joke', JokeElement);
    this.showAsElement(20);
  }

  showAsElement(id: number) {
    const jokeEl: WithProperties<CustomJokeComponent> = document.createElement('custom-joke') as any;
    jokeEl.id = id;
    document.body.appendChild(jokeEl as any);
  }
}

The code in the constructor creates the custom component and attaches it to the DOM with our showAsElement method.

createCustomElement is from our @angular/element code.

The showAsElement method loads our custom-joke Web Component that we defined earlier.

custom-joke.component.ts :

import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core';
import { JokeService } from '../joke.service';

@Component({
  selector: 'custom-joke',
  template: `<p>{{joke?.value?.joke}}</p>`,
  styles: [`p { font-size: 20px }`],
  encapsulation: ViewEncapsulation.Native
})
export class CustomJokeComponent implements OnInit {
  @Input() id: number = 1;
  joke: any = {};

  constructor(private jokeService: JokeService) { }

  async ngOnInit() {
    this.joke = await this.jokeService.getJokeById(this.id)
  }

}

We put everything in one file so they can all be included in our custom-joke Web Component.

The @Input will be converted to an attribute that we can pass a number into and get the joke by its ID.

We leave custom-joke.component.html and app.component.html blank.

Conclusion

We use the @angular/element package to create a Web Component that we can use.

The difference is that we include the template and styles inline.

Also, we have to register the component and attach it to the DOM.

Categories
Web Components

Creating Web Components — Templates and Slots

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable UI blocks that we can use in multiple places.

In this article, we’ll look at how to use templates and slots with Web Components.

Templates

We can use the template element to add content that’s not rendered in the DOM. This lets us incorporate them into any other element by writing the following HTML:

<template>
  <p>Foo</p>
</template>

Then we can write the following JavaScript to incorporate it into the page:

const template = document.querySelector('template');
const templateContent = template.content;
document.body.appendChild(templateContent);

We should see ‘Foo’ on the page when we load the page. The p element should be showing when we inspect the code for ‘Foo’. This confirms that the markup isn’t affected by the template element.

Using Templates with Web Components

We can also use them with Web Components. To use it, we can get the template element in the class for the Web Component just like we do outside the Web Component.

For example, we can write:

customElements.define('foo-paragraph',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.querySelector('template');
      let templateContent = template.content;
      const shadowRoot = this.attachShadow({
          mode: 'open'
        })
        .appendChild(templateContent.cloneNode(true));
    }
  })

In the code above, we get the template element and the call cloneNode on templateContent which has template.content as the value to get the template’s content.

Then we call cloneNode to clone the template.content node’s children in addition to the node itself. This is indicated by the true argument of the cloneNode function call.

Finally, we attached the cloned templateContent into the shadow root of the Web Component with the attachShadow and appendChild methods.

We can also include styling inside a template element, so we can reuse it as we do with the HTML markup.

For instance, we can change the template element to:

<template>
  <style>
    p {
      color: white;
      background-color: gray;
      padding: 5px;
    }

  </style>
  <p>Foo</p>
</template>

Then we should see:

Photo by Zoë Reeve on Unsplash

Slots

To make our templates more flexible, we can add slots to them to display content as we wish instead of static content. With slots, we can pass in content between our Web Component opening and closing tags.

It has more limited support than templates. Slots can be used with Chrome 53 or later, Opera since version 40, Firefox since version 59, and not supported in Edge.

For example, we can add some slots by writing:

<template>
  <slot name="foo">Default text for foo</slot>
  <slot name="bar">Default text for bar</slot>
  <slot name="baz">Default text for baz</slot>
</template>

Then given that we have the same code as before for defining the foo-paragraph element in JavaScript, we can use it as follows:

<foo-paragraph>
  <p slot='foo'>foo</p>
  <p slot='bar'>bar</p>
  <p slot='baz'>baz</p>
</foo-paragraph>

The output then will be:

foo

bar

baz

If we omit the elements inside the foo-paragraph tags, we get the default text:

Default text for foo Default text for bar Default text for baz

In the code above, we name the slots with the name attribute in the template element, then in our Web Component, we fill in the slots that we defined by using the slot attribute.

We can style them like any other template element. To style them, we select the elements to be styled by using the selector for the elements that we’ll fill into the slots.

For example, we can write:

<template>
  <style>
    ::slotted(p) {
      padding: 5px;
      background-color: black;
      color: white;
    }

    ::slotted(p[slot='foo']) {
      background-color: gray;
    }
  </style>
  <slot name="foo">Default text for foo</slot>
  <slot name="bar">Default text for bar</slot>
  <slot name="baz">Default text for baz</slot>
</template>

<foo-paragraph>
  <p slot='foo'>foo</p>
  <p slot='bar'>bar</p>
  <p slot='baz'>baz</p>
</foo-paragraph>

The ::slotted pseudo-element represents elements that has been placed into a slot inside the HTML template. Therefore, we can use it to select the slot elements that we want to style.

In the argument of ::slotted , we can pass in the element that we want to style if they were passed into the slot. This means that only p elements that are passed in will have styles applied to them.

::slotted(p[slot=’foo’]) means that a p element with slot attribute values foo will have styling applied to it.

Then we should get the following after applying the styles:

Templates and slots let us create reusable components that we can use in Web Components. The template element does not show up during render so we can put them anywhere and use them as we wish in any Web Component.

With slots, we have more flexibility when creating Web Components since we can create components that we can insert other elements into to fill the slots.

In any case, we can apply styles to the elements as we wish. We have the ::slotted pseudoelement to apply styles to specific elements in slots.

Categories
Web Components

Creating Web Components — Lifecycle Callbacks

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable UI blocks that we can use in multiple places.

In this article, we’ll look at the lifecycle hooks of Web Components and how we use them.

Lifecycle Hooks

Web Components have their own lifecycle. The following events happen in a Web Component’s lifecycle:

  • Element is inserted into the DOM
  • Updates when a UI event is being triggered
  • Element deleted from the DOM

Web Component has lifecycle hooks are callback functions that capture these lifecycle events and let us handle them accordingly.

They let us handle these events without creating our own system to do so. Most JavaScript frameworks provide the same functionality, but Web Components is a standard so we don’t need to load extra code to be able to use them.

The following lifecycle hooks are in a web component:

  • constructor()
  • connectedCallback()
  • disconnectedCallback()
  • attributeChangedCallback(name, oldValue, newValue)
  • adoptedCallback()

constructor()

The constructor() is called when the Web Component is created. It’s called when we create the shadow DOM and it’s used for setting up listeners and initialize a component’s state.

However, it’s not recommended that we run things like rendering and fetching resources here. The connectedCallback is better for these kinds of tasks.

Defining a constructor is optional for ES6 classes, but an empty one will be created when it’s undefined.

When creating the constructor, we’ve to call super() to call the class that the Web Component class extends.

We can have return statements in there and we can’t use document.write() or document.open() in there.

Also, we can’t gain attributes or children in the constructor method.

connectedCallback()

connectedCallback() method is called when an element is added to the DOM. We can be sure that the element is available to the DOM when this method is called.

This means that we can safely set attributes, fetch resources, run set up code or render templates.

disconnectedCallback()

This is called when the element is removed from the DOM. Therefore, it’s an ideal place to add cleanup logic and to free up resources. We can also use this callback to:

  • notify another part of an application that the element is removed from the DOM
  • free resources that won’t be garbage collected automatically like unsubscribing from DOM events, stop interval timers, or unregister all registered callbacks

This hook is never called when the user closes the tab and it can be trigger more than once during its lifetime.

attributeChangedCallback(attrName, oldVal, newVal)

We can pass attributes with values to a Web Component like any other attribute:

<custom-element
  foo="foo"
  bar="bar"
  baz="baz">
</custom-element>

In this callback, we can get the value of the attributes as they’re assigned in the code.

We can add a static get observedAttributes() hook to define what attribute values we observe. For example, we can write:

class CustomElement extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({
      mode: 'open'
    });
  }

  static get observedAttributes() {
    return ['foo', 'bar', 'baz'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`${name}'s value has been changed from ${oldValue} to ${newValue}`);
  }
}

customElements.define('custom-element', CustomElement);

Then we should the following from the console.log :

foo's value has been changed from null to foo
bar's value has been changed from null to bar
baz's value has been changed from null to baz

given that we have the following HTML:

<custom-element foo="foo" bar="bar" baz="baz">
</custom-element>

We get this because we assigned the values to the foo, bar, and baz attributes in the HTML with the values of the same name.

adoptedCallback()

The adoptedCallback is called when we call document.adoptNode with the element passed in. It only occurs when we deal with iframes.

The adoptNode method is used to transfer a node from one document to another. An iframe has another document, so it’s possible to call this with iframe’s document object.

Example

We can use it to create an element with text that blinks as follows:

class BlinkElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const shadow = this.attachShadow({
      mode: 'open'
    });
    this.span = document.createElement('span');
    this.span.textContent = this.getAttribute('text');
    const style = document.createElement('style');
    style.textContent = 'span { color: black }';
    this.intervalTimer = setInterval(() => {
      let styleText = this.style.textContent;
      if (style.textContent.includes('red')) {
        style.textContent = 'span { color: black }';
      } else {
        style.textContent = 'span { color: red }';
      }

}, 1000)
    shadow.appendChild(style);
    shadow.appendChild(this.span);
  }

  disconnectedCallback() {
    clearInterval(this.intervalTimer);
  }

  static get observedAttributes() {
    return ['text'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'text') {
      if (this.span) {
        this.span.textContent = newValue;
      }

    }
  }
}

customElements.define('blink-element', BlinkElement);

In the connectedCallback , we have all the code for initializing the element. It includes adding a span for the text that we passed into the text attribute as its value, and the styles for blinking the text. The style starts with the creation of the style element and then we added the setInterval code to blink the text by changing the color of the span from red to black and vice versa.

Then we attached the nodes to the shadow DOM which we created at the beginning.

We have the observedAttributes static method to set the attributes to watch for. This is used by the attributeChangedCallback to watch for the value of the text attribute and set it accordingly by getting the newValue .

To see the attributeChangedCallback in action, we can create the element dynamically and set its attribute as follows:

const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
blink2.setAttribute('text', 'baz');

Then we should bar and baz if we log newValue in the attributeChangedCallback hook.

Finally, we have the disconnectedCallback which is run when the component is removed. For example, when we remove an element with removeChild as in the following code:

const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
document.body.removeChild(blink2);

The lifecycle hooks allow us to handle various DOM events inside and outside the shadow DOM. Web Components have hooks for initializing, removing, and attribute changes. We can select which attributes to watch for changes.

There’re also hooks for adopting elements in another document.

Categories
Web Components

Introduction to Creating Web Components

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable components that we can use in multiple places.

Web Components are also isolated from other pieces of code so it’s harder to accidentally modify them from other pieces of code and create conflicting code.

In this article, we’ll look at the different parts of a Web Component and how to create basic ones.

Parts of a Web Component

A Web Component has 3 main parts. Together they encapsulate the functionality that can be reused whenever we like without fear of code conflicts.

  • custom element — a set of JavaScript APIs that allow us to define custom elements and their behavior, which can be used as we desired in the UI
  • shadow DOM — set of JavaScript APIs for attaching the encapsulated shadow DOM tree of the element. It’s rendered separately from the main document DOM. this keeps the element’s features private, so they can be scripted without the fear of conflicting with other parts of the document
  • HTML templates — the template or slot elements enable us to write markup templates that aren’t displayed on the rendered page. They can be reused multiple times as the basis of the custom elements structure.

Creating Web Components

To create Web Components, we do the following steps:

  1. Create a class or function to specify the web component functionality.
  2. Register our new custom element using the CustomElementRegistry.define() method, passing the element name to be defined and the class or function which the functionality is specified, and optionally what element it inherits from
  3. Attach a shadow DOM to the custom element using the Element.attachSHadow() method. Add child elements, event listeners, etc. to the shadow DOM using normal DOM methods
  4. Define HTML templates using the template and slot tags. We use regular DOM methods to clone the template and attach it to our shadow DOM.
  5. Use custom element wherever we like on our page like any other regular HTML element

Basic Examples

The CustomElementRegistry keeps a list of custom elements that are defined. This object lets us register new custom elements on a page and return information about what custom elements are registered etc.

To register a new custom element on a page, we use the CustomElementRegistry.define() method. It takes the following arguments:

  • a string representing the name of the element. A dashed is required to be used in them. They can’t be single words.
  • a class object that defines the behavior of the element
  • an optional argument containing an extends property which specifies the built-in element our element inherits from if any

We can define a custom element as follows:

class WordCount extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({
      mode: 'open'
    });
    const span = document.createElement('span');
    span.textContent = this.getAttribute('text').split(' ').length;
    const style = document.createElement('style');
    style.textContent = 'span { color: red }';
    shadow.appendChild(style);
    shadow.appendChild(span);
  }
}

customElements.define('word-count', WordCount);

In the code above, we attach a shadow DOM to our document by calling attachShadow . mode set to 'open' means that the shadow root is accessible from JavaScript outside the root. It can also be 'closed' which means the opposite.

Then we create a span element, where we set the text content to the text attribute’s length after we split the words in it wherever there’s a space.

Next, we created a style element and then set the content to color: red .

Finally, we attached them both to the shadow DOM root.

Notice that in our class, we extended the HTMLElement . We’ve to do this to define a custom element.

We can use our new element as follows:

<word-count text='Hello world.'></word-count>

There’re 2 types of custom elements. They are:

  • autonomous custom elements — which are standalone and don’t inherit from standard HTML elements. We can create them by referencing the name directly. For example, we can write <word-count>, or document.createElement("word-count").
  • customized built-in elements — these inherit from basic HTML elements. We can extend one of the built-in HTML elements to create these kinds of elements. We can use them by writing <p is="word-count">, or document.createElement("p", { is: "word-count" }).

The kind of custom element we created before are autonomous custom elements. We can create customized elements by writing:

class WordCount extends HTMLParagraphElement {
  constructor() {
    super();
    const shadow = this.attachShadow({
      mode: 'open'
    });
    const span = document.createElement('span');
    span.textContent = this.getAttribute('text').split(' ').length;
    const style = document.createElement('style');
    style.textContent = 'span { color: red }';
    shadow.appendChild(style);
    shadow.appendChild(span);
  }
}

customElements.define('word-count', WordCount, {
  extends: 'p'
});

Then put in our page by writing:

<p is='word-count' text='Hello world.'></p>

As we can see, autonomous custom elements and customized built-in elements aren’t that different. The only differences are that we extend HTMLParagraphElement instead of HTMLElement .

Then we used the is attribute to reference the custom element instead of using the element name in the customized built-in element.

Internal vs. External Styles

We can also reference external styles as follows instead of using internal styles as we had above. For example, we can write:

const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
shadow.appendChild(linkElem);

This references styles from style.css . We created a link element just like we do in HTML and normal DOM manipulation.

Creating Web Components is simple. We just have to define a class or function for the functionality and then put it in the custom elements registry by using the customElements.define method.

We can extend existing elements like p elements or create ones from scratch. Also, we can add internal styles or reference external ones by creating a link element and referencing external files.