Stencil: The power behind Ionic 4

Custom Elements (Web Components) are a thing of the now. The majority of web frameworks now use a component based model because it’s a convenient way to separate concerns, as well as providing reusable, declarative UI.

Here’s a video I made on Stencil a while ago (2017).

Embracing the idea of cross-platform should also be embracing the idea of modularity and developer choice, all of the things the web component model brings. Until recently libraries such as Ionic have had to choose an underlying framework such as Angular and hook into that.

From the perspective of Ionic, the potential audience is significantly limited to strictly the audience of Angular users. This all changed with the introduction of Stencil!

You may want to read my recently published article regarding the history of Ionic for more information on this.

Creating a Stencil project

To create a Stencil project, we can type the following in our terminal:

# Initialise a new Stencil project
$ npm init stencil

# Selection of either ionic-pwa, app or component
Starter > ionic-pwa
Name > stencil-ionic

# Change directory
$ cd stencil-ionic

# Start project
$ code . && npm start

As you can see, this started the Stencil CLI and we were able to use it to generate a new Stencil project.

New Stencil project

This looks familiar. That’s because it’s using @ionic/core for the UI!

Components

Let’s jump in. Head over to src/components/app-home/ we see the following files:

  • app-home.css (styling)
  • app-home.e2e.ts (e2e tests)
  • app-home.spec.ts (unit tests)
  • app-home.tsx (component)

You’ll be happy to see that we’ve got testing right out of the box with Stencil. Open up app-home.tsx to see the component in action:

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

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})
export class AppHome {

  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content padding>
        <p>
          Welcome to the PWA Toolkit. You can use this starter to build entire
          apps with web components using Stencil and ionic/core! Check out the
          README for everything that comes in this starter out of the box and
          check out our docs on <a href="https://stenciljs.com">stenciljs.com</a> to get started.
        </p>

        <ion-button href="/profile/ionic" expand="block">Profile page</ion-button>
      </ion-content>
    ];
  }
}

It uses the best of both worlds, TypeScript decorators to reduce boilerplate and complexity, as well as the standard component model that many are used to.

If you’ve used React, Preact or any other library using .tsx, you’ll have come into contact with this syntax before.

Similar to Angular, when we use the @Component decorator above our Class, we’re letting Stencil know that this class is a component we’re creating.

Let’s create our trusty Todo List with Stencil.

Stateful components

Components that manage their own state using are known as stateful components. We can use stateful components to manage complex behavior(s) and render output. This is in contrast to functional components (which we’ll look at very soon) which simply render markup based on props.

We firstly need to create a TodoList component. This can be done by creating a new folder named app-todo-list inside of src/components. Inside of that, we can create a new file named app-todo-list.tsx.

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

interface Todo {
  name: string;
  completed: boolean;
}

@Component({
  tag: 'app-todo-list'
})
export class AppTodoList {
  @State() todos: Array<Todo> = [{ name: 'Create a todo list', completed: false }];

  @State()
  newTodo: Todo = {
    name: '',
    completed: false
  };

  submitForm = event => {
    event.preventDefault();
    this.addTodo();
  };

  addTodo = () => {
    this.todos = [...this.todos, this.newTodo];

    this.newTodo = {
      name: '',
      completed: false
    };
  };

  handleTodoInputChange = ev => {
    this.newTodo.name = ev.target.value;
  };

  render() {
    return (
      <form novalidate="true" onSubmit={this.submitForm}>
        <ion-item>
          <ion-label>New Todo</ion-label>
          <ion-input value={this.newTodo.name} onInput={this.handleTodoInputChange} type="text" />
          <ion-button onClick={this.addTodo}>Add</ion-button>
        </ion-item>

        <ion-list>
        {
          this.todos.map(
            (todo: Todo) => 
              <ion-item>{todo.name}</ion-item>
            )
        }
        </ion-list>
      </form>
    );
  }
}

The app-home component can then be updated to display our list:

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

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})
export class AppHome {
  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Todo List</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content padding>
        <app-todo-list />
      </ion-content>
    ];
  }
}

Todo List

Functional components

We can create functional components that only display data via passed in props. In our scenario, it may be useful to display a TodoItem using this method.

This is because the TodoItem component doesn’t need to manage any internal state of it’s own. It can simply get the TodoItem as an input, and we can store the todos inside of the TodoList component.

Let’s see this in action!

Create a new folder called app-todo-item and create a file named app-todo-item.tsx inside of it. We can then define our new FunctionalComponent that displays an ion-item based on incoming props.

import { FunctionalComponent } from '@stencil/core';

interface AppTodoItemProps {
  todo: { name: string; completed: boolean };
}

export const AppTodoItem: FunctionalComponent<AppTodoItemProps> = ({ todo }) => <ion-item>{todo.name}</ion-item>;

Functional components aren’t exposed as a Custom Element, so we’ll have to import it inside of our app-todo-list component:

import { AppTodoItem } from '../app-todo-item/app-todo-item';

We can then update our list to display this like so:

<ion-list>
  {
    this.todos.map(
      (todo: Todo) => <AppTodoItem todo={todo} />
    )
  }
</ion-list>

Slots

We can use slots to allow us to make our components more dynamic. Let’s say we wanted to optionally provide content within our list (such as a list header), we’d add a slot tag:

<ion-list>
  <slot />
  {
    this.todos.map(
      (todo: Todo) => <AppTodoItem todo={todo} />
    )
  }
</ion-list>

By adding mark-up inside of the app-todo-list within app-home.tsx, that content will be automatically placed into the slot:

<app-todo-list>
  <ion-list-header>Agenda</ion-list-header>
</app-todo-list>

Slots

Slots are awesome!

Using the ion-router with Stencil components

If we look at src/components/app-root/app-root.tsx, we can see the usage of ion-router and ion-route in action:

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

@Component({
  tag: 'app-root',
  styleUrl: 'app-root.css'
})
export class AppRoot {

  render() {
    return (
      <ion-app>
        <ion-router useHash={false}>
          <ion-route url="/" component="app-home" />
          <ion-route url="/profile/:name" component="app-profile" />
        </ion-router>
        <ion-nav />
      </ion-app>
    );
  }
}

We’re able to specify the component we want to load with the component attribute, as well as the transitioned url. The component attribute needs to match the tag described in your metadata:

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})

An example of component metadata.

Configuration

We can further configure Stencil inside of the stencil.config.ts. In our case, we won’t need to change any of the configuration elements as the out of the box configuration is enough.

import { Config } from '@stencil/core';

// https://stenciljs.com/docs/config

export const config: Config = {
  outputTargets: [{ type: 'www' }],
  globalScript: 'src/global/app.ts',
  globalStyle: 'src/global/app.css'
};

For a full list of all configuration options, check here: Stencil configuration

Conclusion

Stencil gives us the power to create Custom Elements in an easy fashion. Ionic is a great example of what can be done with this. I hope you found this introduction useful! :)