Creating an Elm Project with Snowpack

mickeyvip

Mickey

Posted on November 28, 2020

Creating an Elm Project with Snowpack

Intro

There is a new emerging trend of a development setup in the frontend world. As browsers now natively support esm modules, we don't need to use bundlers like Webpack, Parcel or Rollup.

Projects like Snowpack, Vite (for Vue), Svite (for Svelte) and recently SvelteKit (official one for Svelte) are starting to gain attention.

By skipping the bundling stage, the browser is immediately updated with the changes in the source files, without having to wait for re-bundling.

Snowpack

Described in its own words as:

... a lightning-fast frontend build tool, designed for the modern web.

Snowpack has an out-of-the-box support for TypeScript, JSX, CSS Modules and more. It also can be extended with plugins, and there is a plugin for Elm: Snowpack Elm Plugin by Marc Walter. Thank you Marc!

Today I would like to share a setup of Snowpack with Elm and TailwindCSS - a very popular utility-first CSS framework.

Project Setup

Let's create our project from scratch, as it is described in the documentation' "Starting a New Project"

mkdir elm-snowpack
cd elm-snowpack
npm init -y
npm install snowpack --save-dev
Enter fullscreen mode Exit fullscreen mode

Those will perform several things:

  • create elm-snowpack folder
  • cd into it
  • create package.json with predefined fields without prompting us (the -y or --yes)
  • install Snowpack version 3.x.y.

Now let's add index.html to the root of our project with the following content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <h1>Welcome to Snowpack!</h1>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

and add a start script entry to the package.json file:

  "scripts": {
    "start": "snowpack dev",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
Enter fullscreen mode Exit fullscreen mode

with that ready let's start the Snowpack development server:

npm run start
Enter fullscreen mode Exit fullscreen mode

We see some Snowpack's output:

console output

The browser opens automatically (if it will not - open it manually and navigate to http://localhost:8080).

And we see our beautiful application:

snowpack first run

Following the documentation, let's add an index.js and index.css files.

Create both those files in the root of our project:

index.js

console.log('Hello World!');
Enter fullscreen mode Exit fullscreen mode

index.css

body {
  font-family: sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

And update index.html to include them:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />
    <!-- add index.css -->
    <link rel="stylesheet" type="text/css" href="/index.css" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <h1>Welcome to Snowpack!</h1>
    <!-- add index.js -->
    <script type="module" src="/index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

After saving all the files, the browser is updated with our changes.

browser output with js and css

Reorganizing

Having all the files in the root of our project is not practical. Usually source files reside in src folder. So let's reorganize our files.

Stop the Snowpack development server. Create an src folder and move index.js and index.css there. Then create public folder and move index.html there.

elm-snowpack/
  node_modules/
  public/
    index.html
  src/
    index.css
    index.js
  package.json
  package-lock.json
Enter fullscreen mode Exit fullscreen mode

Since we have moved our files around, we need to inform Snowpack where to find them. Snowpack can be configured from the command line or by adding snowpack.config.js to the project folder. So let's add this file in the root of our project and add the default content from the documentation:

module.exports = {
  plugins: [
    /* ... */
  ],
  installOptions: {
    /* ... */
  },
  devOptions: {
    /* ... */
  },
  buildOptions: {
    /* ... */
  },
  proxy: {
    /* ... */
  },
  mount: {
    /* ... */
  },
  alias: {
    /* ... */
  },
};
Enter fullscreen mode Exit fullscreen mode

This file can also be added automatically by Snowpack:

npx snowpack init
Enter fullscreen mode Exit fullscreen mode

The entry we are interested in is mount and the example setting in the documentation is exactly suits our setup:

// ...omitted
  {
    "mount": {
      "src": "/dist",
      "public": "/"
    }
  }
// ...omitted
Enter fullscreen mode Exit fullscreen mode

You can look for the examples of how this mapping works in the documentation, but basically it maps requests of /dist/* to serve files under src/* (index.js, index.css) and requests of /* to serve files under public/* (index.html).

According to this mapping our index.html will be server from /public folder when requested as / and it's good, but our source files under src/ folder should be requested from /dist. So let's change their URIs in the index.html:

<link rel="stylesheet" type="text/css" href="/dist/index.css" />
Enter fullscreen mode Exit fullscreen mode
<script type="module" src="/dist/index.js"></script>
Enter fullscreen mode Exit fullscreen mode

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />
    <!-- update URI to index.css -->    
    <link rel="stylesheet" type="text/css" href="/dist/index.css" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <h1>Welcome to Snowpack!</h1>
    <!-- update URI to index.js -->
    <script type="module" src="/dist/index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Starting our development server should show our application as before.

Adding Elm Support

Great, we have an empty application running and the changes that we make are propagated to the browser with lightning speed!

Let's add Elm support now.

As I mentioned in the beginning - There is a Snowpack Elm Plugin by Marc Walter.

We need to install it (as a dev dependency using -D flag):

npm install -D snowpack-plugin-elm
Enter fullscreen mode Exit fullscreen mode

And add it to the plugins entry in the snowpack.config.js:

module.exports = {
  // ...
  plugins: ['snowpack-plugin-elm'],
  // ...
}
Enter fullscreen mode Exit fullscreen mode

as it is described in the plugin's repo.

Good, now let's add src/Main.elm with the mandatory "Hello World!":

module Main exposing (main)

import Html exposing (Html)

main =
    Html.text "Hello, World!"
Enter fullscreen mode Exit fullscreen mode

and import our Elm application in src/index.js:

import Elm from './Main.elm';

console.log(Elm);

console.log('Elm and Tailwind with Snowpack');
Enter fullscreen mode Exit fullscreen mode

If we run our application now (or it was already running) we'll see an error message that, very rightfully, tells us that we didn't initialize an Elm project with elm.json file:

Elm not initialized

So let's do exactly that:

elm init
Enter fullscreen mode Exit fullscreen mode

Click Enter to agree with the prompt. This will create the elm.json file.

Running the application again shows no errors. And we see in the console that the Elm application is indeed created:

Alt Text

The wiring of the Elm application is described in the guide's JavaScript Interop/Embedding in HTML. We can copy the code from there to our src/index.js (I have deleted all the console.log):

import Elm from './Main.elm';

const app = Elm.Main.init({
  node: document.getElementById('app'),
});
Enter fullscreen mode Exit fullscreen mode

We also need to update our index.html file to have a placeholder for our Elm application:

<div id="app"></div>
Enter fullscreen mode Exit fullscreen mode

So now our index.html looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />  
    <link rel="stylesheet" type="text/css" href="/dist/index.css" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/dist/index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

After saving the file the browser is instantly updated and shows the "Hello, World!" - our application is working!

Alt Text

Try editing the text and saving it. The browser updates really fast!

Adding Tailwind CSS

Now let's add a Tailwind CSS to our project. We partially follow the Installing Tailwind CSS as a PostCSS plugin as it described in the documentation, only instead of postcss we'll install postcss-cli, that Snowpack needs.

npm install -D tailwindcss autoprefixer postcss-cli
Enter fullscreen mode Exit fullscreen mode

The default presets of Tailwind CSS are absolutely sufficient to start playing with prototyping components/design. But in order to further customize Tailwind CSS we will need a configuration file - tailwind.config.js. So as an optional step we can create it by running:

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

Let's replace the content of our src/index.css with the standard TailwindCSS presets:

@tailwind base;

@tailwind components;

@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Let's also create postcss.config.js file:

module.exports = {
  plugins: [
    require('tailwindcss'),
    require('autoprefixer'),
  ],
};
Enter fullscreen mode Exit fullscreen mode

We tell PostCSS to use tailwindcss and autoprefixer (that is another plugin that add browser specific prefixes for the relevant CSS rules).

Good, now we need to tell Snowpack to execute the PostCSS when needed. Following the official guide on PostCSS, update snowpack.config.js, plugins entry:

plugins: [
  [
    '@snowpack/plugin-build-script',
    { cmd: 'postcss', input: ['.css'], output: ['.css'] },
  ],
  'snowpack-plugin-elm',
],
Enter fullscreen mode Exit fullscreen mode

And that should enable TailwindCSS in our application.

NOTE: There is also an official way to setup TailwindCSS with Snowpack, but it did not work for me.

I took a chunk of user card HTML from the Tailwind CSS website's (version 1.9) landing page and pasted it in our public/index.html. In addition, I have updated the avatar image with an online placeholder and added shadow-lg mx-10 to the <div> in order to see the shadow and for the card not to spread all the way to the edges of the window:

<!-- ... -->
<body>
  <div id="app"></div>
  <div class="md:flex bg-white rounded-lg p-6 shadow-lg m-10">
    <img
      class="h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6"
      src="https://i.pravatar.cc/100?img=1"
    />
    <div class="text-center md:text-left">
      <h2 class="text-lg">Erin Lindford</h2>
      <div class="text-purple-500">Product Engineer</div>
      <div class="text-gray-600">erinlindford@example.com</div>
      <div class="text-gray-600">(555) 765-4321</div>
    </div>
  </div>
<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

And after the page updates, we see Tailwind CSS in action:

user card long

This style is responsive. Try narrowing the browser's window and see the card changes:

user card short

We can re-create this HTML in our Elm application and delete the raw HTML:

module Main exposing (main)

import Browser
import Html exposing (Html, div, h2, img, text)
import Html.Attributes exposing (class, src)


type alias User =
    { fullName : String
    , position : String
    , email : String
    , phone : String
    , avatar : String
    }


erinLindford : User
erinLindford =
    { fullName = "Erin Lindford"
    , position = "Product Engineer"
    , email = "erinlindford@example.com"
    , phone = "(555) 765-4321"
    , avatar = "https://i.pravatar.cc/100?img=1"
    }


userCardView : User -> Html msg
userCardView user =
    div [ class "md:flex bg-white rounded-lg p-6 shadow-lg m-10" ]
        [ img
            [ class "h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6"
            , src user.avatar
            ]
            []
        , div
            [ class "text-center md:text-left" ]
            [ h2 [ class "text-lg" ] [ text user.fullName ]
            , div [ class "text-purple-500" ] [ text user.position ]
            , div [ class "text-gray-600" ] [ text user.email ]
            , div [ class "text-gray-600" ] [ text user.phone ]
            ]
        ]


update : msg -> User -> User
update _ model =
    model


view : User -> Html msg
view model =
    userCardView model


main : Program () User msg
main =
    Browser.sandbox { init = erinLindford, update = update, view = view }
Enter fullscreen mode Exit fullscreen mode

Delete the raw HTML from index.html and refresh the page - the result is the same as before.

And at last we have a nice development setup with Snowpack, Elm and TailwindCSS.

Although the step-by-step tutorial may seem long, it's pretty quick to re-create this setup.

Hope you enjoyed creating our small project and find this useful.

Thank you for reading.

💖 💪 🙅 🚩
mickeyvip
Mickey

Posted on November 28, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related