Creating a custom dropdown component with Stencil

September 21, 2017, 10:29 am Categories:

Categories

Stencil is a new web component compiler created by the Ionic team with the following goals:

  • Framework agnostic - not restricted by or tied to a particular front-end framework (will play nicely with all frameworks)
  • Generates standards compliant web components
  • Utilises additional API's such as Virtual DOM, JSX and async rendering
  • Ideally suited for Progressive web Apps (runs well on slow/fast networks, small code size, fast execution/rendering times, interoperable across different browsers & devices)

Ionic 4 which, at the time of writing, is still under development will make use of components generated exclusively by Stencil (no small task given the user base for Ionic - definitely a testament to the team's faith in their compiler).

The benefits to developers include generating custom components, a speed boost in terms of rendering/execution times as well as offering the opportunity for alternative frameworks to be used in place of Angular (that's going to be a huge sell for interoperability within development teams).

Stencil provides these improvements to Web Component generation through use of the following features:

  • Async rendering - Allows parts of a component state to be rendered asynchronously (which )
  • JSX - JavaScriptXML allowing DOM nodes to be constructed with HTML-like syntax
  • Reactive data-binding - Implements data-binding through binding state variables to onChange events allowing those states to be changed as the input values change
  • TypeScript - A superset of JavaScript providing class based programming features and strong data typing
  • Virtual DOM - A tree of custom objects representing a part of the DOM allowing that to be acted upon quicker than directly manipulating the DOM itself

That's a pretty impressive feature set isn't it?

One question though: how well supported is the Web Component standard? Or, to put it another way, can we use this now?

Caveats

As with any new technology browser support is always a consideration for developers looking to implement certain tools in their projects.

Fortunately Web Components, as of the v1 specification, are supported natively in Google Chrome and Safari (with support soon to arrive in both Firefox and Edge). Stencil provides a custom elements polyfill that is dynamically loaded on older browsers where that support is needed.

Overall then browser support for Stencil is (as of September 2017):

Current support
Browser Status
Edge with polyfill
Firefox with polyfill
Chrome Native support
IE11+ with polyfill
Safari Native support

What we'll be building

With any language/technology words are only as good as the practice that goes into using them which is why, over the course of this tutorial, we'll spend time familiarising ourselves with Stencil's unique blend of syntax to create the following simple dropdown component:

Custom drop-down menu component in a closed state

Once opened the dropdown component will then display information like so:

Custom drop-down menu component in an open state

Nothing terribly fancy or complicated but it will provide a nice introduction to working with Stencil.

The component is more of an accordion than an actual dropdown but as there's only one segment to the component that can be opened/hidden I've stuck to calling this a dropdown instead (the joy of semantics - feel free to term this whatever you feel works best).

Along the way we'll explore the different types of syntax used within a Stencil component including:

  • Decorators
  • Events
  • JSX for template rendering

Most of which, if you've been using Ionic/Angular over the last year, will, unsurprisingly, be quite familiar (and this is, of course, a good thing as it will help get developers up to speed by lowering the barrier to entry when working with a new tool).

Okay, so we've had a brief look at what Stencil is (we'll cover the syntax and features in more depth over the remainder of this tutorial) and we know what we'll be developing.

Ready to begin?

Off we go!

The anatomy of a Stencil component

A basic Stencil component is created by adding a new file with a .tsx extension into a sub-directory named after the component which is located within a src/components directory, for example:

- src/components

  // custom component directory
  - my-dropdown 

    // Web component files for the custom component
    -my-dropdown.tsx
    -my-dropdown.scss

The scss file will be familiar to most Ionic developers (depending on their experience with the framework) and this provides any style rules that are required by the custom component.

The logic and templating for the component is provided by the .tsx file (as Stencil components are built using a combination of JSX and TypeScript the .tsx file extension is required).

So what does the logic/templating for a Stencil component look like then?

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


@Component({
  tag: 'my-name',
  styleUrl: 'my-name.scss'
})
export class MyName {

  @Prop() first: string;

  @Prop() last: string;

  render() {
    return (
      <div>
        Hello, my name is {this.first} {this.last}
      </div>
    );
  }
}

This would then be implemented within a template view (the index.html file of the src directory) like so:

<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
  <title>Stencil Starter App</title>

  <!-- The published component logic is brought in through this transpiled JavaScript -->
  <script src="/build/app.js"></script>

  <meta name="theme-color" content="#16161d">

  <meta name="apple-mobile-web-app-capable" content="yes">
  <link rel="apple-touch-icon" href="/assets/img/icon.png">

  <link rel="manifest" href="/manifest.json">
  <link rel="icon" type="image/x-icon" href="/assets/icon/favicon.ico">

</head>
<body>

  <!-- The custom component is entered here -->
  <my-name first="Stencil" last="JS"></my-name>

  </body>
</html>

When the application is built for production (we'll cover this process in the next section) the component files are transpiled into JavaScript and published to the www directory (similar to how Ionic publishes production ready code).

Let's go over the format and syntax for the component.tsx file (most of which will be familiar to Ionic/Angular developers).

We begin our Stencil component by importing the required node packages/modules:

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

Following from this the component decorator then defines the metadata for the component; the name of the HTML tag to be used for rendering the component and what external styles are to be used:

@Component({
  tag: 'my-name',
  styleUrl: 'my-name.scss'
})

Next the component class is declared with the following elements:

  • 2 public properties (named first and last) which are set by the user in the rendered HTML of the component
  • The render method contains JSX code (a combination of JavaScript and HTML-like syntax) which returns the HTML representation of what we want pushed to the DOM
export class MyName {

  @Prop() first: string;

  @Prop() last: string;

  render() {
    return (
      <div>
        Hello, my name is {this.first} {this.last}
      </div>
    );
  }
}

The public properties are entirely optional for a component but the render method is required for template rendering.

Over the remainder of this tutorial we'll be developing a dropdown component and investigate additional features of Stencil development including:

  • Events
  • State management
  • Dynamic content

Before we do though let's begin by installing Stencil and familiarising ourselves with the necessary CLI commands.

Beginning Stencil development

Currently, as of mid-September 2017, a Stencil project is created by cloning the app-base from Github followed by installing the node packages required by the compiler:

git clone https://github.com/ionic-team/stencil-starter.git my-app
cd my-app
git remote rm origin
npm install

Stencil components can then be run inside the browser (using a live-reload server - which means any development changes that are made will be automatically reflected in real-time) with the following command:

npm start

Once development has been completed a production build can be run (which will generate a minified distribution of all the Stencil components that you've created for that project) using the following command:

npm run build

Creating a dropdown component

Within the my-app/src/components directory create a new directory named my-dropdown.

Inside of the my-dropdown directory add the following files:

  • my-dropdown.tsx
  • my-dropdown.scss

These are going to be the source files that contain all of the component logic, templating and styling.

Within the my-dropdown.tsx file enter the following code (this is fully commented to explain what is happening at each key stage of the code - along with some examples of managing state and event changes borrowed from Cory Rylan's excellent Stencil tutorial):

/**
 * Import the following modules:
 * 
 * 1. Component - Provide/declare component metadata
 * 2. Event - Manage component events
 * 3. EventEmitter - Allow events to be broadcast within the component
 * 4. Prop - Allows public properties to be used inside the component logic
 * 5. State - Manage component state (I.e.respond to changes)
 *
 */
import { Component, Event, EventEmitter, Prop, State } from '@stencil/core';


/**
 *
 * Define the component metadata:
 * 1. Name of the HTML tag to be rendered
 * 2. The external style rules to be applied for the component
 */
@Component({
	tag: 'my-dropdown',
	styleUrl: 'my-dropdown.scss'
})
export class myDropdown {

        /**
         * @public
         * @property items
         *
         * Defines the data that we want to load into our dropdown area
         * 
         */
	public items : Array<any> = [
	   {  heading: 'Virtual DOM', 
	      description: 'A tree of custom objects representing a part of the DOM which can be acted upon quicker than manipulating the DOM itself'
	   },
	   {
	      heading: 'Async rendering', 
	      description : 'Allows parts of a component state to be rendered asynchronously (I.e. via XHR)'
	   },
	   {
	      heading : 'Reactive data-binding',
	      description : 'Allows data binding to be implemented through binding a state variable to an onChange event which allows the state to be changed as the input value changes'
	   },
	   {
	      heading : 'TypeScript',
	      description : 'A superset of JavaScript providing strong typing and class based programming constructs'
	   },
	   {
	      heading : 'JSX',
	      description : 'JavaScriptXML allows DOM nodes to be built with HTML-like syntax'
	   }
	];


    /**
     * @public
     * @property name
     * @type String
     *
     * This will accept values supplied by a name attribute 
     * on the component HTML
     */
    @Prop() name       : string;



    /**
     * @type boolean
     *
     * This will track state changes (I.e. whether the 
     * dropdown component is open or closed)
     */
    @State() toggle    : boolean = false;



    /**
     * @type EventEmitter
     *
     * Track component events (I.e. activation of dropdown component)
     */
    @Event() onToggle  : EventEmitter;



    /**
     * @public
     * @method toggleComponent
     *
     * This will manage the dropdown component event and state changes
     */
    toggleComponent(): void
    {
       this.toggle = !this.toggle;
       // When the user click emit the toggle state value
       // A event can emit any type of value
       this.onToggle.emit({ visible: this.toggle });
    }



    /** 
     * Create HTML representation of component DOM and return this
       for output to the browser DOM
     */
    render() {
       return (
	  <div>
             // Perform following:
             // 1. Assign click event to heading tag to manage dropdown component activation
             // 2. Assign value for public property of name as the text for the <h2> tag
             // 3. Change which arrow 'icons' are displayed based on state change
	     <h2 onClick={() => this. toggleComponent()}>{this.name} {this.toggle ? <span>&#9650;</span> : <span>&#9660;</span>}</h2> 
             
             // Use value of state change to determine whether component information 
             // is displayed
	     <ul class={ this.toggle ? 'active' : 'inactive' }>

                // Build each component item with data supplied from the items 
                // array using a map operator
	        {this.items.map(item => <li><h3>{item.heading}</h3><p>{item.description}</p></li>)}
	     </ul>
          </div>
       )
    }
}

With the my-dropdown.tsx aspect of the component covered in-depth let's now move onto the style rules for the my-dropdown.scss component file:

my-dropdown {

   h2 {
      cursor: pointer;
      position: relative;
      padding: 0 0.35em;
      font-size: 1.35em;
      font-family: Verdana;


      // Define the style rules for the 'arrow icons'
      span {
         position: absolute;
         right: 1em;
         top: 0.75em;
         font-size: 0.5em;
      }
   }


   // Here we define the actual 'menu' and its 'options'
   ul {
      list-style: none;
      padding: 0;

      li {
         background: rgba(230, 230, 230, 1);
         border-bottom: 1px solid rgba(200, 200, 200, 1);
         padding : 0.5em 1em;

         h3 {
            // -webkit rules for Safari
            -webkit-margin-before: 0.3em;
            -webkit-margin-after: 0.3em;
            line-height: 1.2em;
            font-family: Verdana;
            font-size: 1rem;
         }

         p {
            line-height: 1.2em;
            margin: 0 0 1em 0;
            font-family: Verdana;
            font-size: 1rem;
         }
      }
   }


   // Following classes display/hide the 'menu'
   // based on the state change detection in the 
   // component class
   .active {
      display: block;
   }

   .inactive {
      display: none;
   }
}

There's one final step we need to take before we can see our component in action and that's to edit the src/index.html file.

Within the <body> section of this file perform the following changes:

  • Remove or comment out the default <my-name> component
  • Add the <my-dropdown> component (not forgetting to supply the name attribute along with a value)

Your src/index.html file should, after these edits have been made, look like the following (your value for the name attribute may differ if you've decided to add your own content):

<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
  <title>Stencil Starter App</title>
  <script src="/build/app.js"></script>

  <meta name="theme-color" content="#16161d">

  <meta name="apple-mobile-web-app-capable" content="yes">
  <link rel="apple-touch-icon" href="/assets/img/icon.png">

  <link rel="manifest" href="/manifest.json">
  <link rel="icon" type="image/x-icon" href="/assets/icon/favicon.ico">

</head>
<body>

  <!--my-name first="Stencil" last="JS"></my-name-->
  <my-dropdown name="Stencil key features"></my-dropdown>

  </body>
</html>

With development now completed we can test/view the component in the browser (if you haven't already being doing so) with the following command:

npm start

Which, if no errors have been thrown up by the compiler, should give us output akin to the following (with the component activated):

Custom drop-down menu component in an open state

Pretty cool huh?

That's your first Stencil component generated and gives you an introduction to how this compiler can be used to create 'framework-less' Web Components.

In summary

As you have learnt developing components with Stencil is quite quick and easy to accomplish. The online documentation is fairly solid (although a few more diverse examples of different features and their implementation wouldn't go unappreciated) and the forthcoming release of Ionic 4 with components generated purely with Stencil makes working with the component compiler a solid investment moving forwards.

There's a lot more I could have covered in the above tutorial such as the component lifecycle, configuration, server side rendering and distribution but these are topics for future tutorials.

My aim with this article was to introduce you to Stencil and how that can be used to generate/compile Web Components using a best-of-breed approach to front-end development.

When Ionic 4 is released I'll publish further articles detailing Stencil component generation and how to implement those within your own Ionic projects but until then I hope you enjoyed this tutorial and please feel free to leave feedback in the comments section below.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top