Phoenix 1.6 Easy PETAL Stack Setup (w/ Custom Font)

Simple PETAL Setup (Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView)

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 requrements - 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 creat 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 Phoenix install with LiveView"

credo

add credo to mix.exs

{:credo, "~> 1.6"},
{:phx_gen_tailwind, "~> 0.1.3", only: :dev, runtime: false}

configure Credo & TailwindCSS

mix deps.get
mix credo gen.config
mix phx.gen.tailwind

Test TailwindCSS

add to the top of: lib/slacker_gen_tail_web/templates/page/index.html.heex

<div class="text-green-500 text-5xl text-center">Large Centered Green TailwindCSS</div>

start phoenix mix -S phx.server

install AlpineJS

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);
      }
    },
  },
});

now change the loading of assets/app.js in the root file to load assets/js/app.js to load AlpineJS into Phoenix you do that with in lib/fonts_web/templates/layout/root.html.heex by changing:

    <!-- 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>

to:

    <!-- change this from app.js to js/app.js -->
    <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/js/app.js")}></script>

so now lib/fonts_web/templates/layout/root.html.heex should look like:

<!-- 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" %>
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
    <!-- change the `javascript` static path from `/assets/app.js` to `/assets/app.js` to load AlpineJS -->
    <%# <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>

Test AlpineJS

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!

Add CSS Components to TailwindCSS

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

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

Put your default site CSS here.

Let’s also create some custom CSS components:

# 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

Let’s add our components (& Default CSS to our config)

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

/* custom styles - put after base imports! */
@import "./default.css";
/* import custom components */
@import "./components/buttons.css";
/* default phoenix styles - replaced bby default.css file */
/* @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>

Start phoenix with:

iex -S mix phx.server

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

Add a custom Font

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

Find a font at: https://fonts.google.com/ (or any other source of fonts)

I will use handlee as it is very distinctive - to make the setup easier I will download handlee from: https://google-webfonts-helper.herokuapp.com/fonts/handlee?subsets=latin

I will put the fonts in the folder: assets/fonts/handlee

mkdir -p assets/fonts/handlee
touch assets/fonts/handlee/handless.css
cat <<EOF>>assets/fonts/handlee/handless.css
/* handlee-regular - latin - copied from: https://google-webfonts-helper.herokuapp.com/fonts/handlee?subsets=latin */
/* be sure the font-path is local to where the fonts are */
@font-face {
  font-family: 'Handlee';
  font-style: normal;
  font-weight: 400;
  src: local(''),
       url('handlee-v12-latin-regular.woff2') format('woff2'),
       url('handlee-v12-latin-regular.woff') format('woff');
}
EOF

Now lets setup esbuild to manage the CSS and fonts - the default in config/config.exs is:

# config/config.exs
config :esbuild,
  version: "0.14.0",
  default: [
    args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

We will need to add our FontCSS path fonts/handlee/handlee.css to the build System along with the font types to load:--loader:.woff2=file --loader:.woff=file, so now it should look like:

config :esbuild,
  version: "0.14.0",
  default: [
    args: ~w(js/app.js fonts/handlee/handlee.css --bundle --loader:.woff2=file --loader:.woff=file --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

Now you NEED to restart phoenix once a config file changes!

Assuming everything is working you will see:

  • the fonts files in: priv/static/assets/
  • the fonts css in: priv/static/assets/fonts/handlee/handlee.css

this is critical - if this isn’t working you MUST fix it!

Integrate the font into CSS (& TailwindCSS)

Now you will need to tell TailwindCSS about the font so you can use: font-handprint as a CSS class.

Your initial assets/tailwind.config.js file will look like:

module.exports = {
  mode: 'jit',
  purge: [
    './js/**/*.js',
    '../lib/*_web/**/*.*ex'
  ],
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Your define you font in the theme section with:

module.exports = {
  mode: 'jit',
  purge: [
    './js/**/*.js',
    '../lib/*_web/**/*.*ex'
  ],
  theme: {
    extend: {
      fontFamily: {
        handprint: ['Handlee']
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Working in Switzerland it is convenient to use very multi-lingual fonts such as Noto and redefine the sans and serif fonts - in fact, my goto fonts and setup are:

module.exports = {
  mode: 'jit',
  purge: [
    './js/**/*.js',
    '../lib/*_web/**/*.*ex'
  ],
  theme: {
    extend: {
      fontFamily: {
        comic: ['ComicNeue'],
        hand: ['Handless'],
        sans: ['NotoSans', 'sans-serif'],
        serif: ['NotoSerif', 'serif']
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Integrate the Fonts Into Phoenix

in the root.html.heex file add the static route /assets/fonts/Hanlee/hanlee.css before the /assets/app.css.

This is done with the following html route:

    <!-- add font before app.css -->
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/fonts/Hanlee/hanlee.css")}/>
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>

So now lib/fonts_web/templates/layout/root.html.heex would look like:

<!-- 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 BEFORE `/assets/app.css` -->
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/fonts/Hanlee/hanlee.css")}/>
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
    <!-- change the `javascript` static path from `/assets/app.js` to `/assets/app.js` to load AlpineJS -->
    <%# <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>

Test you new font

Now add to the file lib/slacker_gen_tail_web/templates/page/index.html.heex to the following html:

<p class="text-red-500 text-5xl text-center font-handprint">Hand Printed</p>

Ideally you now see your new font displayed within the “Hand Printed” text

NOTE: if you get an error like: (Phoenix.Router.NoRouteError) no route found for GET /assets/fonts/Handlee/handlee.css

Check your esbundle config and that priv/static/assets/fonts/handlee/handlee.css is being copied (Static routes always point to: priv/static/)!

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