DEV Community

Rúnar Berg Baugsson Sigríðarson
Rúnar Berg Baugsson Sigríðarson

Posted on • Originally published at github.com

<my-button>: My First Stencil Component

We are going to start with the simplest of the three components
<my-button>. Honestly it might be an overkill writing this out as a
component as it will be a simple styled wrapper around the native
<button>. But the purpose here is to get you acquainted with Stencil
before we write out more involved components:

We can get the component scaffolded by writing in our terminal:

npm run generate my-button
Enter fullscreen mode Exit fullscreen mode

The Stencil tool will ask you if you want to create a stylesheet, spec
tests, and E2E tests for your component as well. We’re not focising on
automated tests for this tutorial, so you can uncheck the E2E and spec
test options. Leave the stylesheet option checked though, we’ll use it
later:

> my-components@0.0.1 generate /home/username/dropdown-menu
> stencil generate "my-button"

? Which additional files do you want to generate? ›
Instructions:
    ↑/↓: Highlight option
    ←/→/[space]: Toggle selection
    a: Toggle all
    enter/return: Complete answer
◉   Stylesheet (.css)
◯   Spec Test  (.spec.tsx)
◯   E2E Test (.e2e.ts)
Enter fullscreen mode Exit fullscreen mode

Open the newly created src/components/my-button.tsx. You will see it
contains a few imports from the @stencil/core and an exported class
decorated with @Component. You’ll also notice that the class
contains a single render method.

Now let’s start the development server again:

npm run start
Enter fullscreen mode Exit fullscreen mode

Let’s make the <my-button> elements actual buttons. So dig into the
render method and wrap a <button> around the <slot>.

import { Component, ComponentInterface, Host, h } from "@stencil/core";

@Component({
  tag: "my-button",
  styleUrl: "my-button.css",
  shadow: true,
})
export class MyButton implements ComponentInterface {
  render() {
    return (
      <Host>
        <button>
          <slot></slot>
        </button>
      </Host>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Refresh your browser window and see we now have buttons.

There are a few takeaways here. First is the <slot>
element
we put inside the <button>. Slots let
consumers write their own markup inside our component. Notice how our
consumer in src/index.html says:

<my-button>Action 1</my-button>
Enter fullscreen mode Exit fullscreen mode

Well… Our <slot> will be replaced by the text “Action 1”. Slots can
include custom markup as well, meaning if a consumer writes:

<my-button>
  <strong>Warning</strong>
  This is <em>dangerous</em>!
</my-button>
Enter fullscreen mode Exit fullscreen mode

The correct parts will by strong and emphasized. Go ahead and try it!

The next new takeaway here is the <Host> element.
This is a functional component provided by Stencil. We can use it to
set attributes and listeners to the host element (in our case
<my-button>). We’re not really using it for anything now but it does
us no harm to leave it standing.

Thirdly is the render method. This method—as the name
suggests—renders our component to the DOM tree. We can think of it as
the means to keep our component in sync with all the state we provide
to it. We can pass conditionals, loops, or reactive data inside it and
it will render the component to match whatever the state is at any
time.

And Finally we have the @Component decorator.
This will tell the Stencil compiler to create a web component out of
this class. We provide this decorator with a tag name my-button, and
optionally with a path to a stylesheet (which we’ll be using in a
moment). The shadow: true part makes sure our component is isolated
from the rest of the DOM tree. If we set it to false we risk global
styles bleeding into our component, or an ID collision with a
different element on the page.

Styling the Button

Phew! Now lets do something more fun. Open my-button.css and make
our button a lot prettier. Notice the default style says:

:host {
  display: block;
}
Enter fullscreen mode Exit fullscreen mode

The :host pseudo-class refers to the
my-button element it self (the host). We don’t really care how it is
displayed, so lets remove the display rule. But the :host block is
handy for defining our custom properties (or CSS
variables).

:host {
  --background: var(--button-background, ivory);
  --background-hover: var(--button-background-hover, gold);
  --color: var(--button-color, rebeccapurple);
}

button {
  background: var(--background);
  border: 2px solid currentcolor;
  border-radius: 1ex;
  color: var(--color);
  cursor: pointer;
  padding: 0.5ex 1em;
}

button:hover {
  --background: var(--background-hover);
}
Enter fullscreen mode Exit fullscreen mode

Refresh the page and now our buttons look much nicer.

Notice how we define our custom properties in terms of other custom
properties:

--color: var(--button-color, rebeccapurple);
Enter fullscreen mode Exit fullscreen mode

This is so a parent can set the --button-color and it will overwrite
our default value (rebeccapurple). Unlike other styles custom
properties will cascade down into the shadow dom.

You can try it by opening the style inspector in your browser’s
developer tools and adding a rule to you root html element:

--button-color: firebrick;
Enter fullscreen mode Exit fullscreen mode

Passing Properties

Sometimes buttons can be disabled. Say that an action is not allowed
at the moment then disabling the button will prevent user frustration
when they click the button and nothing happens. Lets disable the last
button in the demo menu (index.html).

<!-- src/index.html -->

<my-menu>
  <!-- ... -->
  <my-menu-item>
    <my-button disabled>Action 3</my-button>
  </my-menu-item>
</my-menu>
Enter fullscreen mode Exit fullscreen mode

Uh-oh! We can still click it, what’s wrong? It turns out that we
neglected to define what happens when our custom button has any
attributes. We can fix that by importing the @Prop
decorator
and adding it to our class.

First lets add some styles so we can better tell when we have
succeeded our mission:

/* src/components/my-button/my-button.css */

:host {
  /* ... */
  --background-disabled: var(--button-background-disabled, ivory);
  --color-disabled: var(--button-color-disabled, thistle);
}

/* ... */

button:disabled {
  --background: var(--background-disabled);
  --color: var(--color-disabled);

  cursor: not-allowed;
}
Enter fullscreen mode Exit fullscreen mode

Next we must import the @Prop decorator from @stencil/core which
we use to decorate a new boolean property in our class named
disabled. We’ll then use this new property to conditionally set the
disabled attribute on the child button in the render function:

// src/components/my-button/my-button.tsx

import { Prop /* ... */ } from "@stencil/core";

@Component(/* ... */)
export class MyButton implements ComponentInterface {
  // Setting reflect to true, adds the attribute on the host
  // element (`<my-button>`) as well.
  @Prop({ reflect: true }) disabled: boolean;

  render() {
    return (
      <Host>
        <button disabled={this.disabled}>
          <slot></slot>
        </button>
      </Host>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Mirroring a string attribute is similar. This time type which
defaults to submit:

export class MyButton implements ComponentInterface {
  @Prop({ reflect: true }) disabled: boolean;
  @Prop({ reflect: true }) type: "button" | "reset" | "submit" = "submit";

  render() {
    return (
      <Host>
        <button disabled={this.disabled} type={this.type}>
          <slot></slot>
        </button>
      </Host>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Lets add one more property weight which allows us to control the
visual weight of the button (slim, normal, or strong):

export class MyButton implements ComponentInterface {
  @Prop({ reflect: true }) disabled: boolean;
  @Prop({ reflect: true }) type: "button" | "reset" | "submit" = "submit";
  @Prop() weight: "slim" | "normal" | "strong" = "normal";

  render() {
    const classMap = {
      "weight-slim": this.weight === "slim",
      "weight-normal": this.weight === "normal",
      "weight-strong": this.weight === "strong",
    };

    return (
      <Host>
        <button class={classMap} disabled={this.disabled} type={this.type}>
          <slot></slot>
        </button>
      </Host>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
/* src/components/my-button/my-button.css */

/* ... */

button.weight-slim {
  border: none;
  background: transparent;
  padding: 0;
  text-decoration: underline;
}

button.weight-strong {
  background: var(--color);
  border-color: var(--color);
  color: white;
}
Enter fullscreen mode Exit fullscreen mode

Feel free to alter the attributes your <my-button>s in
src/index.html to test out these new properties.

Discussion (0)