Phoenix 1.6 PETAL Stack Setup by Hand

Simple PETAL Setup

I have been enjoying the tools associated with Elixir and exploring the frontend. LiveView helps make that more intuitive and when that isn’t enough, AlpineJS is a lightweight JS tool with a similar syntax as Vue.

Install asdf - and required software

On a Mac I used Homebrew:

brew install asdf
echo -e '\n. $(brew --prefix asdf)/asdf.sh' >> ~/.bash_profile
echo -e '\n. $(brew --prefix asdf)/etc/bash_completion.d/asdf.bash' >> ~/.bash_profile
source ~/.bash_profile  # (or open a new terminal)

Now you can install asdf software packages:

asdf plugin-add erlang
asdf plugin-add elixir
asdf plugin-add Postgres

Now you need to install the desired versions (usually the newest) - currently:

asdf list all erlang
asdf install erlang 24.2.2
asdf global erlang 24.2.2


# note the elixir version otp must match the erlang version!
asdf list all elixir
asdf install elixir 1.13.3-otp-24
asdf global elixir 1.13.3-otp-24

# asdf install elixir 1.11.4-otp-24
# if you mismatch elixir with erlang you will get errors like:
# {"init terminating in do_boot",{undef,[{elixir,start_cli,[],[]},{init,start_em,1,[]},{init,do_boot,3,[]}]}}

asdf list all Postgres
asdf install Postgres 14.2

Get the newest Elixir tools

mix local.rebar --force
mix local.hex --force

Get the newest Phoenix Hex Package

Once you have established you have the requirements - the download the newest version of Phoenix (go to: https://hexdocs.pm/phoenix/installation.html#phoenix to see the newest version) - at the time of this writing its 1.5.8 - be sure its installed using:

mix archive.install hex phx_new 1.6.6 --force

create a project with asdf settings

First we will create the folder / project location

mkdir petal

Now we will tell it which software to use:

touch petal/.tool-versions
cat <<EOF >>petal/.tool-versions
erlang 24.2.2
elixir 1.13.3-otp-24
Postgres 14.2
EOF

Create a new Phoenix Project

https://carlyleec.medium.com/create-an-elixir-phoenix-app-with-asdf-e918649b4d58

Now you can simply do:

mix phx.new petal --live
cd petal
mix ecto.create

assuming all is good lets configure git:

git init
git add .
git commit -m "initial Phoneix install with LiveView"

credo

add credo to mix.exs

{:credo, "~> 1.6"}

configure credo

mix deps.get
mix credo gen.config

install & test Alpine JS

https://www.youtube.com/watch?v=vZBHkvTAb2U https://sergiotapia.com/phoenix-160-liveview-esbuild-tailwind-jit-alpinejs-a-brief-tutorial

cd assets
npm install alpinejs
cd ..

Now change app.js is to require our new setup:

# assets/js/app.js
// .. after the app.scss import add:
import Alpine from "alpinejs";

still in assets/js/app.js find:

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

and change to:

// after all the imports
window.Alpine = Alpine;
Alpine.start();
let hooks = {};
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  hooks: hooks,
  dom: {
    onBeforeElUpdated(from, to) {
      if (from._x_dataStack) {
        window.Alpine.clone(from, to);
      }
    },
  },
});

TEST by adding to the end of: lib/petal_web/live/page_live.html.leex

<section>
  <h2>Alpine JS Installed</h2>
  <div x-data="{name:''}">
    <label for="name">Name:</label>
    <input id="name" type="text" x-model="name" />
    <p><br><b><em>Output:</em></b> <span x-text="name"></span></p>
  </div>
</section>

test with: mix phx.server

when typing the name should appear below!

let’s snapshot:

git add .
git commit -m "phoenix with alpine js"

Integrating Tailwind into phoenix

https://www.youtube.com/watch?v=vZBHkvTAb2U https://pragmaticstudio.com/tutorials/adding-tailwind-css-to-phoenix https://sergiotapia.com/phoenix-160-liveview-esbuild-tailwind-jit-alpinejs-a-brief-tutorial

lets install tailwind:

cd assets
npm install autoprefixer postcss postcss-import postcss-cli tailwindcss --save-dev
cd ..

comment out import "../css/app.css" to avoid pipeline compilation conflicts

// assets/js/app.js
// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
// import "../css/app.css"

now create & configure postcss.config.js

cat <<EOF>assets/postcss.config.js
module.exports = {
  plugins: {
    "postcss-import": {},
    tailwindcss: {},
    autoprefixer: {},
  }
}
EOF

now configure tailwind with (to purge *.js, *.eex, *.leex, *.heex files)

cat <<EOF >assets/tailwind.config.js
module.exports = {
  mode: "jit",
  purge: [
    "./js/**/*.js",
    "../lib/*_web/**/*.*ex"
  ],
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

EOF

in config/dev.exs update the watcher with:

# config/dev.exs
watchers: [
  esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
  npx: [
    "tailwindcss",
    "--input=css/app.css",
    "--output=../priv/static/assets/app.css",
    "--postcss",
    "--watch",
    cd: Path.expand("../assets", __DIR__)
  ]
]

to BUILD ASSETS in Production add in deps/phoenix/package.json:

"scripts": {
  "deploy": "NODE_ENV=production tailwindcss --postcss --minify --input=css/app.css --output=../priv/static/assets/app.css"
}

and change this to:

  "scripts": {
    "deploy": "NODE_ENV=production webpack --mode production",
    "watch": "webpack --mode development --watch"
  },

we will create a file for our custom styles the assets/css/default-styles.css file:

# assuming you are in the root directory
touch assets/css/default-styles.css

Let’s also create our a custom component (we will make buttons for a counter to be sure tailwind and aplineJS are playing well together):

# assuming you are still in the assets directory on the cli
mkdir assets/css/components
touch assets/css/components/buttons.css
cat <<EOF >assets/css/components/buttons.css
@layer components {
  .btn-redish {
    @apply bg-red-300 hover:bg-red-600 text-blue-800 font-bold py-2 px-4 rounded;
  }
  .btn-greenish {
    @apply bg-green-300 hover:bg-green-600 text-blue-800 font-bold py-2 px-4 rounded;
  }
}
EOF

Now will will configure Phoenix to load Tailwind, our custom-styles and our custom-components – DO THIS AT THE TOP OF the file assets/css/app.scss (@imports must be before all else):

/* Import tailwind - with postcss-import installed */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

/* custom styles - put after base imports! */
@import "./default-styles.css";
/* import custom components */
@import "./components/buttons.css";
/* default phoenix styles - eventually remove */
@import "./phoenix.css";

add a test html from tailwind to the end of: lib/petal_web/live/page_live.html.leex

<section class="grid grid-cols-1 gap-4">
	<!-- tailwind text -->
  <div>
    <h2 class="text-red-500 text-5xl font-bold text-center">Tailwind CSS with Alpine JS Dropdown</h2>
  </div>
  <!-- alpinejs dropdown test -->
	<div x-data="{ open: false }" class="relative text-left">
  	<button
  					@click="open = !open"
  					@keydown.escape.window="open = false"
  					@click.away="open = false"
  					class="flex items-center h-8 pl-3 pr-2 border border-black focus:outline-none">
  			<span class="text-sm leading-none">
  					Options
  			</span>
  			<svg class="w-4 h-4 mt-px ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
  					<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
  			</svg>
  	</button>
  	<div
  					x-cloak
  					x-show="open"
  					x-transition:enter="transition ease-out duration-100"
  					x-transition:enter-start="transform opacity-0 scale-95"
  					x-transition:enter-end="transform opacity-100 scale-100"
  					x-transition:leave="transition ease-in duration-75"
  					x-transition:leave-start="transform opacity-100 scale-100"
  					x-transition:leave-end="transform opacity-0 scale-95"
  					class="absolute flex flex-col w-40 mt-1 border border-black shadow-xs">
  			<a class="flex items-center h-8 px-3 text-sm hover:bg-gray-200" href="#">Settings</a>
  			<a class="flex items-center h-8 px-3 text-sm hover:bg-gray-200" href="#">Support</a>
  			<a class="flex items-center h-8 px-3 text-sm hover:bg-gray-200" href="#">Sign Out</a>
  	</div>
	</div>

  <!-- alpinejs counter test -->
  <div>
    <p class="mt-5 font-bold text-center">Counter with Component Buttons</p>
  </div>
  <!--
    If you want a box around the counter use:
    <div class="flex items-center justify-center h-screen bg-gray-200">
  -->
  <div class="mt-10 flex justify-center" x-data="{ count: 0 }">
    <button class="btn-redish" x-on:click="count--">Decrement</button>
    <code>count: </code><code x-text="count"></code>
    <button class="btn-greenish" x-on:click="count++">Increment</button>
  </div>
</section>

Test with

iex -S mix phx.server

Now we should have a dropdown & our colored component buttons on our counter.

now lets snapshot our PETAL setup:

git add .
git commit -m "PETAL 1.6.x Configured"

Add a custom Font

https://experimentingwithcode.com/custom-fonts-with-phoenix-and-tailwind/

We will download 2 Fonts:

Noto Sans

We will start with Noto - listed on Google fonts from Webfonts Helper

Copy the CSS from the website (prefer modern browswers if possible) - update the Customize folder prefix

mkdir -p assets/vendor/fonts/NotoSans
cat <<EOF>assets/vendor/fonts/NotoSans/noto_sans.css
/* noto-sans-regular - latin-ext_latin */
@font-face {
  font-family: 'Noto Sans';
  font-style: normal;
  font-weight: 400;
  src: local(''),
       url('../fonts/NotoSans/noto-sans-v25-latin-ext_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
       url('../fonts/NotoSans/noto-sans-v25-latin-ext_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
EOF

Quickens (otf)

Download and convert the font at:

mkdir -p assets/vendor/fonts/Quickens
cat <<EOF>assets/vendor/fonts/Quickens/quickens.css
/* otf not recommended - convert the font at:
  * https://transfonter.org/
  * https://onlinefontconverter.com/
  * https://www.fontsquirrel.com/tools/webfont-generator

@font-face {
  font-family: 'Quickens';
  font-style: normal;
  font-weight: 400;
  src: local(''),
       url('quickens-regular.otf') format('opentype'),
       url('quickens-rough.otf') format('opentype');
} */

@font-face {
  font-family: 'Quickens';
  font-style: normal;
  font-weight: 400;
  src: local(''),
       url('quickens_regular-webfont.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
       url('quickens_regular-webfont.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

@font-face {
  font-family: 'QuickensRough';
  font-style: normal;
  font-weight: 400;
  src: local(''),
       url('quickens_rough-webfont.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
       url('quickens_rough-webfont.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
EOF

Add Fonts to HTML

# /lib/fonts_web/templates/layout/root.html.heex
<!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.0"/>
    <%= csrf_meta_tag() %>
    <%= live_title_tag assigns[:page_title] || "SlackerPetal", suffix: " · Phoenix Framework" %>
    <!-- add font here -->
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/vendor/fonts/NotoSans/noto_sans.css")}/>
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/vendor/fonts/NotoSerif/noto_serif.css")}/>
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/vendor/fonts/Quickens/quickens.css")}/>
    <!-- end custom fonts -->
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
    <!-- change this from app.js to js/app.js -->
    <%# <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script> %>
    <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/js/app.js")}></script>
  </head>
  <body>
    <header>
      <section class="container">
        <nav>
          <ul>
            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
            <%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
              <li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
            <% end %>
          </ul>
        </nav>
        <a href="https://phoenixframework.org/" class="phx-logo">
          <img src={Routes.static_path(@conn, "/images/phoenix.png")} alt="Phoenix Framework Logo"/>
        </a>
      </section>
    </header>
    <%= @inner_content %>
  </body>
</html>

OOPS (Phoenix.Router.NoRouteError) no route found for GET /assets/vendor/fonts/NotoSerif/noto_serif.css

Update the esbuild configuration

update args in

# /config/config.exs
# Configure esbuild (the version is required)
config :esbuild,
  version: "0.12.18",
  default: [
    args: ~w(js/app.js vendor/fonts/NotoSans/noto_sans.css vendor/fonts/Quickens/quickens.css --bundle --loader:.woff2=file --loader:.woff=file --target=es2017 --outdir=../priv/static/assets),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

be careful --target=es2017 in also --target=es2016 in some Phoenix versions, but best to upgrade.

Update the Tailwind configuration

The final step is to update Tailwind (set NotoSans to the default font & make quickens available)

# /assets/tailwind.config.js
const defaultTheme = require('tailwindcss/defaultTheme')

module.exports = {
  mode: 'jit',
  purge: [
    './js/**/*.js',
    '../lib/*_web/**/*.*ex'
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['NotoSans var', ...defaultTheme.fontFamily.sans],
        quicken: ['Quickens']
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Resources (1.6.x)

Older Resources

Bill Tihen
Bill Tihen
Developer, Data Enthusiast, Educator and Nature’s Friend

very curious – known to explore knownledge and nature