Carl Rippon

Building SPAs

Carl Rippon
BlogBooksAbout
This site uses cookies. Click here to find out more

Creating a Modal Dialog Web Component in StencilJS

August 08, 2018
stenciljs

In the last post we created a modal dialog web component using the standard custom element and shadow DOM APIs.

As mentioned in a previous post, there are quite a few tools popping up that help us create web components. One of those tools that seems to be getting some traction recently is StencilJS. In this post we’ll create the same modal dialog as in the last post using StencilJS. We’ll hopefully discover some of the benefits of using StencilJS over the standard APIs along the way.

Modal dialog

What is StencilJS?

The StencilJS website says it’s a “magical, reusable web component compiler”. So, basically it generates standard web components.

Getting started

So, let’s get started using the docs and start find out why StencilJS is so great …

git clone https://github.com/ionic-team/stencil-component-starter x-modal
cd x-modal
git remote rm origin
npm install

We can run this using:

npm start

Starter Component

Any changes that we make to the code are automatically reflected in the running web app.

Nice!

If we browse to the sample component code at “src\components\my-component” we see “my-component.tsx”. If we look at this file we see the following code:

import { Component, Prop } from "@stencil/core";

@Component({
  tag: "my-component",
  styleUrl: "my-component.css",
  shadow: true
})
export class MyComponent {
  @Prop() first: string;
  @Prop() last: string;

  render() {
    return (
      <div>
        Hello, World! I'm {this.first} {this.last}
      </div>
    );
  }
}

We can see it’s using TypeScript to add typing to the props. We can also see that it is using JSX to render the component’s HTML.

Neat!

Let’s also have a look at “my-component.spec.ts”. These are unit tests for the component using Jest. We can also see stencil is providing a testing utility, TestWindow to help us write tests.

import { TestWindow } from '@stencil/core/testing';import { MyComponent } from './my-component';

describe('my-component', () => {
  ...

  describe('rendering', () => {
    let element: HTMLMyComponentElement;
    let testWindow: TestWindow;    beforeEach(async () => {
      testWindow = new TestWindow();      element = await testWindow.load({        components: [MyComponent],        html: '<my-component></my-component>'      });    });

    ...

    it('should work with a first name', async () => {
      element.first = 'Peter';
      await testWindow.flush();      expect(element.textContent.trim()).toEqual('Hello, World! I\'m Peter');
    });

    ...
  });
});

This is all great - we can already start to see some benefits of stencil.

Let’s create a new folder for our modal dialog component called “x-modal” in “src/components”. Let’s then create a files called “x-modal.tsx” and “x-modal.css” in this folder.

Let’s paste in the following code to “x-modal.tsx”:

import { Component } from "@stencil/core";

@Component({
  tag: "x-modal",
  styleUrl: "x-modal.css",
  shadow: true
})
export class XModal {
  public render(): JSX.Element {
    return <div>TODO - create a modal component!</div>;
  }
}

If we find and open “index.html” and reference x-modal instead of my-component. We’ll see our component displayed.

...
<body>
  <x-modal title="Important!" visible>
	  <p>This is some really important stuff</p>
	</x-modal>
</body>
...

Step 1

This is a good start.

Modal dialog specification

A reminder of what we need to build …

Our modal dialog should look like the image below and the markup that we have just put in “index.html” should be used to render the component.

Modal dialog

So, our tag name is x-modal. A title attribute lets us define a title for the dialog. The content inside the x-modal tag will be the content of the dialog.

A visible attribute will determine whether the dialog is open or closed.

This dialog will have “Okay” and “Cancel” buttons which will invoke events called “ok” and “cancel” respectively.

Our basic web component

In “x-modal.css”, let’s paste in some css:

.wrapper {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: gray;
  opacity: 0;
  visibility: hidden;
  transform: scale(1.1);
  transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
  z-index: 1;
}
.visible {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
  transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
}
.modal {
  font-family: Helvetica;
  font-size: 14px;
  padding: 10px 10px 5px 10px;
  background-color: #fff;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 2px;
  min-width: 300px;
}
.title {
  font-size: 18px;
}
.button-container {
  text-align: right;
}
button {
  margin-left: 5px;
  min-width: 80px;
  background-color: #848e97;
  border-color: #848e97;
  border-style: solid;
  border-radius: 2px;
  padding: 3px;
  color: white;
  cursor: pointer;
}
button:hover {
  background-color: #6c757d;
  border-color: #6c757d;
}

In “x-modal.tsx”, let’s update our render method:

public render(): JSX.Element {
  return (
    <div class="wrapper visible">
      <div class="modal">
        <span class="title">We need to add a title here</span>
        <div class="content">We need to add some content here</div>
        <div class="button-container">
          <button class="cancel">Cancel</button>
          <button class="ok">Okay</button>
        </div>
      </div>
    </div>
  );
}

Our component should now look like:

Modal dialog step 1

Adding a title attribute

Adding a title attribute is super easy:

import { Component, Prop } from "@stencil/core";

...
export class XModal {
  @Prop() public title: string;
  ...
}

We can then reference this in the render function.

public render(): JSX.Element {
  return (
    <div class="wrapper visible">
      <div class="modal">
        <span class="title">{this.title}</span>        <div class="content">We need to add some content here</div>
        <div class="button-container">
          <button class="cancel">Cancel</button>
          <button class="ok">Okay</button>
        </div>
      </div>
    </div>
  );
}

“Slotting” the content

There’s no magic when adding the slot. We simply add the slot tag as we would when using the native APIs:

public render(): JSX.Element {
  return (
    <div class="wrapper visible">
      <div class="modal">
        <span class="title">{this.title}</span>
        <div class="content">
          <slot />        </div>
        <div class="button-container">
          <button class="cancel">Cancel</button>
          <button class="ok">Okay</button>
        </div>
      </div>
    </div>
  );
}

Adding a visible attribute

Let’s add the visible attribute now.

@Prop({
  mutable: true,
  reflectToAttr: true
})
public visible: boolean;

It needs to be “mutable” because we will be changing the value of this property to false when the buttons are clicked. We have also flagged “reflectToAttr” because we want to update the DOM with any changes to this attribute.

We can now reference this property in our render function:

public render(): JSX.Element {
  return (
    <div class={this.visible ? "wrapper visible" : "wrapper"}>      ...
    </div>
  );
}

Cool. We are making good progress. Our rendered component should now look like below. Notice also that the visible attribute automatically opens and closes the dialog.

Modal dialog

Button handlers

On to the button handlers now. We can use the onClick property on the buttons and set this to delegate functions. So no need for addEventListener!

private handleCancelClick = () => {  this.visible = false;};
private handleOkClick = () => {  this.visible = false;};
public render(): JSX.Element {
  return (
    <div class={this.visible ? "wrapper visible" : "wrapper"}>
      ...
        <div class="button-container">
          <button class="cancel" onClick={this.handleCancelClick}>            Cancel
          </button>
          <button class="ok" onClick={this.handleOkClick}>            Okay
          </button>
        </div>
       ...
    </div>
  );
}

The buttons should now close the dialog.

Expose the events

Our final task is to raise “ok” and “cancel” events when the buttons are clicked. We do this by declaring the event and variables with the @Event decorator. We can then simply call .emit at the appropriate point to raise the event.

import { Component, Event, EventEmitter, Prop } from "@stencil/core";
...
export class XModal {
  ...
  @Event() private ok: EventEmitter;  @Event() private cancel: EventEmitter;
  private handleCancelClick = () => {
    this.visible = false;
    this.cancel.emit();  };

  private handleOkClick = () => {
    this.visible = false;
    this.ok.emit();  };
  ...
}

With our events in place, we can attach them as usual in “index.html”:

<script>
  var modal = document.querySelector("x-modal");
  modal.addEventListener("ok", function () {
    console.log("ok");
  });
  modal.addEventListener("cancel", function () {
    console.log("cancel");
  });
</script>

Modal dialog events

Our modal dialog is now complete!

Wrap up

Here are some of the benefits of using StencilJS to produce standard web components (over using the custom element and shadow DOM APIs directly):

  • Generally the code is more concise and easier to read
  • TypeScript gives us type safety
  • The @prop decorator greatly simplifies how we deal with attributes and properties
  • JSX gives us reactive data binding. With the raw APIs we need to do the data binding manually
  • JSX also allows “onSomething” event handlers rather than wiring up event listeners

In summary it brings a modern web development experience to producing web components!