Make phoenix use svelte

September, 28, 2019

In this blogpost I want to show how to make phoenix use svelte in templates. So lets start:

Edit your asset/package.json file and add these 3 lines to devDependencies - also remove uglifyjs - we will replace it with terser which is a maintained fork of uglify

+    "svelte": "^3.12.1",
+    "svelte-loader": "^2.13.6",
+    "terser-webpack-plugin": "^2.1.1",
-    "uglifyjs-webpack-plugin": "^1.2.4",

Next run npm install after everything is installed we can move to the webpack setup: remove uglify, import terser and add the initialization:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = (env, options) => ({
  optimization: {
    minimizer: [
      new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
      new TerserPlugin(),
      new OptimizeCSSAssetsPlugin({})
    ]
  },

Next add a resolve section:

  entry: {
    './js/app.js': ['./js/app.js']
  },
  resolve: {
    alias: {
      svelte: path.resolve('node_modules', 'svelte')
    },
    extensions: ['.mjs', '.js', '.svelte'],
    mainFields: ['svelte', 'browser', 'module', 'main'],
    modules: ['node_modules']
  },

Add also these two - the mjs part is needed because otherwise webpack throws an error that no require function can be used in the browser

        use: {
          loader: 'babel-loader'
        },
      },
      {
        test: /\.mjs$/,
        include: /node_modules/,
        type: "javascript/auto",
      },
      {
        test: /\.(html|svelte)$/,
        exclude: /node_modules/,
        use: {
          loader: 'svelte-loader',
          options: {
            hotReload: true
          }
         }
       }
     ]

with this setup we can include svelte templates to our views In /assets/js/app.js add

import './svelte.js';

In /assets/js/svelte.js with this content:

import App from './App.svelte'

const app = new App({
    target: document.querySelector('.svelte'),
    props: {
        name: 'svelte'
    }
})
export default app

And in /assets/js/App.svelte add:

<script>
    export let name;
</script>
<h1>Phoenix and {name}!</h1>

last thing what we need to do is to add an element where this can be attached to our view:

<div class="svelte"></div>

This should render a h1 tag with the content Phoenix and svelte Thats the basic setup but what if we want more??

Lets add a view helper in lib/myapp_web/views/layout_view.ex add these functions:

  def svelte(name, props) do
    :div
    |> tag([data: [props: json(props)], id: generate_id(name)])
  end

  def json(props) do
    props
    |> Jason.encode
    |> case do
      {:ok, message} -> message
      {:error, _} -> ""
    end
  end

  def generate_id(name) do
    "svelte-#{String.replace(name, " ", "-")}-root"
  end

this allows us to use a helper in our templates:

<%= svelte "test", %{:name => "svelte"} %>

we can pass as many params in this map as we want then we need to create a directory assets/js/svelte and inside create new files with the same name as the first param passed to the svelte helper: assets/js/svelte/test.svelte with normal svelte style templating:

 <script>
    export let name;
 </script>
 
<h1>Phoenix and {name}!</h1>

the last part is to let out app know how to fin these templates: For this to work lets replace the content of assets/js/svelte.js with:

const context = require.context("./svelte", false, /\.svelte/);
window.onload = function() {
  context.keys().forEach((file) => {
    const componentName = file.replace(/\.\/|\.svelte/g, '');
    const targetId = `svelte-${componentName}-root`;

    const root = document.getElementById(targetId);

    if(!root){
      return;
    }

    const requiredApp = require(`./svelte/${componentName}.svelte`);

    const props = root.getAttribute('data-props');
    let parsedProps = {};
    if(props){
      parsedProps = JSON.parse(props);
    }

    new requiredApp.default({
      target: root,
      props: parsedProps
    });
  });
};

Thats all - diffs of both commits can be found in this gist

Previous: Blog setup

Next: Phoenix routes setup