FullStack Setup Phoenix, Elm, GraphQL, and Tailwindcss

Source code: https://github.com/cas27/fs_todo/tree/v1

Show Notes

We are going to build a todo app using emerging web technologies. The tech stack will consist of:

  • Elixir and the Phoenix Framework on the back-end
  • Tailwindcss, Elm, and Webpack 4 on the front-end
  • GraphQL as the api language between them

Phoenix and Webpack 4

First up let's setup a new Phoenix app and replace Brunch with Webpack 4.

$ mix phx.new fs_todo

$ cd fs_todo && mix ecto.create

We are going remove brunch and use Webpack as it is the default in Phoenix 1.4.

$ rm assets/brunch-config.js

Now we need to edit our assets/package.json to use Webpack and remove brunch.

{
  "repository": {},
  "license": "MIT",
  "scripts": {
    "deploy": "webpack --mode production",
    "watch": "webpack --mode development --watch"
  },
  "dependencies": {
    "phoenix": "file:../deps/phoenix",
    "phoenix_html": "file:../deps/phoenix_html"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.3",
    "babel-preset-env": "^1.6.1",
    "copy-webpack-plugin": "^4.5.0",
    "css-loader": "^0.28.10",
    "mini-css-extract-plugin": "^0.4.0",
    "optimize-css-assets-webpack-plugin": "^4.0.0",
    "style-loader": "^0.20.2",
    "uglifyjs-webpack-plugin": "^1.2.4",
    "webpack": "4.4.0",
    "webpack-cli": "^2.0.10"
  }
}

Let's download the Webpack config and babelrc from Phoenix master.

$ curl https://raw.githubusercontent.com/phoenixframework/phoenix/master/installer/templates/phx_assets/webpack/webpack.config.js > assets/webpack.config.js
$ curl https://raw.githubusercontent.com/phoenixframework/phoenix/master/installer/templates/phx_assets/webpack/babelrc > assets/.babelrc

We'll want Phoenix to watch for file changes so we’ll need to update our watcher to use Webpack instead of Brunch

# config/dev.exs
config :fs_todo, FsTodoWeb.Endpoint,  
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
    node: [
      "node_modules/webpack/bin/webpack.js",
      "--mode",
      "development",
      "--watch-stdin",
      cd: Path.expand("../assets", __DIR__)
    ]
  ]

Finally lets install the npm packages we'll need.

$ yarn install

Now that we have everyone configured let's start our dev server and make sure it's all working.

$ mix phx.server

Elm

Now that we have Webpack working let's get Elm added.

$ cd assets && yarn add elm-webpack-loader --dev

Edit our webpack.config.js to have it compile Elm files.

   {
        test: /\.elm$/,
        exclude: ["/elm-stuff/", "/node_modules"],
        loader: "elm-webpack-loader",
        options: {
          debug: true,
          warn: true,
          cwd: path.resolve(__dirname, 'elm')
        }
      }
 ],
 noParse: [/.elm$/]

Now we need to install the Elm packages and give them a place to live.

$ mkdir -p assets/elm/src && cd assets/elm && elm-package install

Let's write a simple Elm app to make sure everything is working together.

-- assets/elm/src/Main.elm 

module App exposing (..)

import Html exposing (..)

main =  
  text "Hello from Elm"

Finally let's edit our view and have our Elm app embedded into the page

<!-- lib/fs_todo_web/templates/layout/app.html.eex -->

<!DOCTYPE html>  
<html lang="en">  
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello FsTodo!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
  </head>

  <body>
        <%= render @view_module, @view_template, assigns %>
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>  
<!-- lib/fs_todo_web/templates/page/index.html.eex -->  
<div id="elm-main">  
</div>  
// assets/js/app.js

import Elm from "../elm/src/Main.elm";

const ELM_DIV = document.getElementById("elm-main");  
if (ELM_DIV) {  
  Elm.App.embed(ELM_DIV);
}

Now we can checkout our app in the browser to make sure we can see our text on the page.

Tailwind CSS

For styling our app we are going to use the utility first CSS framework tailwind CSS. Let's download the npm packages we'll need and add it to Webpack.

$ cd assets && yarn add tailwindcss postcss-loader autoprefixer --dev

Since the tailwind css is generated completely through javascript we'll need its config file.

$ ./node_modules/.bin/tailwind init

Now that we have tailwind's config let's tell Webpack to process it using postcss

// assets/webpack.config.js

 {
   test: /\.css$/,
   use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
  }
// assets/postcss.config.js

module.exports = {  
  plugins: [
    require('tailwindcss')('./tailwind.js'),
    require('autoprefixer')
  ]
}

Last thing we need to do is have our app.js import our css and include the tailwind hooks in our app.css

// assets/js/app.js

import css from "../css/app.css";  
// assets/css/app.css

@tailwind preflight;
@tailwind components;
@tailwind utilities;

Finally lets add some styling toMain.elm` to make sure it's working.

-- assets/elm/src/Main.elm 

module App exposing (..)

import Html exposing (..)

main =  
  text "Hello from Elm"

GraphQL

Instead of using a restful API for our Elm app to talk to the back-end we are going to use GraphQL via the Absinthe library. To get that installed we'll need to add it to our mix.exs and download the dependencies.

<!-- lib/fs_todo_web/templates/layout/app.html.eex -->

<!DOCTYPE html>  
<html lang="en">  
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello FsTodo!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
  </head>

  <body>
        <%= render @view_module, @view_template, assigns %>
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>  

$ mix deps.get

A very helpful tool in development is the GraphiQL web interface to play around with your GraphQL enpoint. Let's get that setup now.

<!-- lib/fs_todo_web/templates/page/index.html.eex -->  
<div id="elm-main">  
</div>  

Now that we have the our route setup to use GraphiQL we'll need to create our schema and load some dummy data in it.

// assets/js/app.js

import Elm from "../elm/src/Main.elm";

const ELM_DIV = document.getElementById("elm-main");  
if (ELM_DIV) {  
  Elm.App.embed(ELM_DIV);
}

Let's head over to our browser at http://localhost:4000/graphiql and do a query our data using GraphQL.

{
  todo(id: 2){
    description
  }
}

Now that we have everything setup, next up we'll style our basic app with Tailwind CSS.