Elixir, Phoenix, and StencilJS

February 12th 2020

I have been a huge fan of the Ionic team’s web component compiler, StencilJS. I have been using it since release for small projects as a full framework, but I recently became interested in it’s potential for design systems work and how it could be integrated into more traditional environments. As a proponent for tranditional multi-page applications, I wanted a way to use a modern javascript development workflow without needing to make special considerations to the architecture of the system. Stencil components are a perfect fit for this role, they can be developed entirely in isolation to each other and my web framework (Phoenix) can use them like any other html tag.

To get this all working nicely, there are a few changes to the default Phoenix configuration that need to be made.

Let’s start with a brand new phoenix project, without webpack because the stencil cli will be our bundling tool.

$ mix phx.new stencil --no-webpack
$ cd stencil

Once that is done generating, we can create our own asset pipeline. You can do this with the stencil cli for application generation.

$ npm init stencil

You will get a prompt asking you what type of project you want to make, select “component” from the list since we will be creating a collection of web components. I usually name my project “assets” so it creates the ./assets folder for me, the standard location for Phoenix js/css code. With the project generated, we need to configure the Phoenix code reloader to detect changes to our frontend code so it auto-refreshes on changes. Change the StencilWeb.Endpoint configuration in config/dev.exs as follows:

config :stencil, StencilWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false
  watchers: [
    node: [
      "node_modules/.bin/stencil",
      "build",
      "--dev",
      "--watch",
      cd: Path.expand("../assets", __DIR__)
    ]
  ]

Now that Phoenix knows to start the stencil compiler when it boots up the server, we need to configure stencil so it will put the compiled code into a place where Phoenix can find it. By default, stencil puts compiled code in the dist folder inside the project, but Phoenix expects static code to live in the priv/static folder. Change your stencil.config.ts file so it puts the compiled components in priv/static.

export const config: Config = {
  namespace: 'components', // This is the name of the compiled file, I like to call it components but you can call it whatever you like
  globalStyle: 'src/global/app.css', // A place to hold global style definitions not specific to a component. You will need to create this file.
  outputTargets: [
    {
      type: 'dist',
      esmLoaderPath: '../loader',
      dir: '../priv/static'
    }
  ]
}

That configuration will create the following directory structure (assuming your namespace in stencil.config.ts is components):

priv
  static
    components
      components.js
      components.esm.js
      components.css

Because the default Phoenix configuration assumes a filename of app.js and app.css for your application’s static files, you need to change the paths in the default layout. Change lib/stencil_web/templates/layouts/app.html.eex to the following:

<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/components/components.css") %>"/>
<script type="module" src="<%= Routes.static_path(@conn, "/components/components.esm.js") %>"/></script>
<script nomodule="" src="<%= Routes.static_path(@conn, "/components/components.js") %>"/></script>

The last change we need to make is to include the components folder in the whitelist of folders that Plug.Static will serve. Modify your endpoint.ex:

plug Plug.Static,
  at: "/",
  from: :stencil,
  gzip: false,
  only: ~w(components css js fonts images favicon.ico robots.txt)

Now when you run mix phx.server the stencil compiler should put everything in the right location and your components should be available on any page. Modify the lib/stencil_web/templates/page/index.html.esx to use the pre-generated component to make sure it works. Delete the auto-generated content in that file with:

<my-component first="Rokkin" last="Cat"></my-component>

With that change, when you load up http://localhost:4000 you should see a page with “Hello World! I’m Rokkin Cat”

Using web components with Phoenix framework makes it possible to utilize a ton of cool features of Phoenix while still getting the benefit of a component-based dev environment. For example, your components can be rendered seemlessly with LiveView, you can create Phoenix.HTML wrappers for your components, or even put a Phoenix channel into your state tunnel for websocket connections.

RokkinCat

is a software engineering agency. We build applications and teach businesses how to use new technologies like machine learning and virtual reality. We write about software, business, and business software culture.