I have previously written about a tech stack I've been feeling particularly productive with, which includes Phoenix LiveViews and Tailwind. As with any tech stack there some details on how to put all the parts together that are good to put in writing and share, in the hope it makes using it that much faster and painless.
The Phoenix App
The very first thing we'll need is a Phoenix App, that will glue everything
together. There are plenty of resources on how to do that online,
an eventually be able to run mix phx.new sample
, to create our app, named
Sample.
At this point, after following the guides, we should have a working Phoenix App, which comes with LiveView by default.
Surface
In my other article, I
also mentioned Surface as a library that plays well with
LiveView, we can install it pretty easily, by adding it to the deps in
mix.exs
:
defp deps do
[
...
{:surface, "~> 0.9.1"}
]
end
Note: At the time of writing this version of surface uses a version of live_view that is above the one in the Phoenix default installation. That means you might have to bump some versions of live_view related packages.
{:phoenix_live_reload, "~> 1.3", only: :dev},
{:phoenix_live_view, "~> 0.18.2"},
{:phoenix_live_dashboard, "0.7.0"},
With this upgrade ensure that you don't have gettext
as one of you compilers,
meaning that your project/0
function in mix.exs
should look like this:
def project do
[
app: :sample,
version: "0.1.0",
elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
Lastly, ensure your views have access to the functions that have been moved to
Phoenix.Component
, by importing it in view/0
at lib/sample_web.ex
.
def view do
quote do
use Phoenix.View,
root: "lib/sample_web/templates",
namespace: SampleWeb
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
# Import reusable function components with HEEx templates
import Phoenix.Component
# Include shared imports and aliases for views
unquote(view_helpers())
end
end
Our first LiveView
With all of this set up, we can add our first LiveView, which will be a simple page with an on/off switch that will change the from normal to dark mode. Before we add the styling though, we will add the markup and event handling. This should be enough to test if everything is working correctly.
Personally I prefer to have the templates have their own files, so our LiveView will be pretty barebones for now:
defmodule SampleWeb.Pages.DarkmodeLive do
use SampleWeb, :live_view
end
All the behaviour comes from that use
call, which is the next thing we need to
change because we want to use Surface.LiveView
instead of Phoenix.LiveView
,
so go to lib/sample_web.ex
and make sure all of the references to Phoenix are
changed to Surface:
def live_view do
quote do
use Surface.LiveView,
layout: {SampleWeb.LayoutView, :live}
unquote(view_helpers())
end
end
def live_component do
quote do
use Surface.LiveComponent
unquote(view_helpers())
end
end
def component do
quote do
use Surface.Component
unquote(view_helpers())
end
end
Template
As I've said, we will add a separate file for the template. That file should be
called the exact same as our component, but have the .sface
extension. For now
it will be as simple as this:
<div>
<h1>Hello World!</h1>
</div>
Routes
We have everything ready, but no way of accessing it yet. For that we need to
add a live route to lib/sample_web/router.ex
.
scope "/", SampleWeb do
pipe_through :browser
get "/", PageController, :index
live "/darkmode", Pages.DarkmodeLive, :index, as: :darkmode
end
That's it! We can now visit http://localhost:4000/darkmode
and we should have
our LiveView working.
Tailwind
Last but not least we need to tailwind, so we can start styling our LiveView. There are many ways you can do this, but I found that the tailwind phoenix library works quite well.
defp deps do
[
...
{:tailwind, "~> 0.1.9", runtime: Mix.env() == :dev},
]
end
Once it is installed, you have to configure it in config/config.exs
:
config :tailwind,
version: "3.2.4",
default: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
Run mix tailwind.install
, which will download the specified version of
Tailwind as well as create an assets/tailwind.config.js
file, which you can
read about in Tailwind's official docs. We do need to tell it about .sface
files, so it knows which classes we are using and only actually import those.
Make sure you tailwind config looks like this:
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
let plugin = require("tailwindcss/plugin")
module.exports = {
content: [
"./js/**/*.js",
"../lib/*_web.ex",
"../lib/*_web/**/*.*ex",
"../lib/*_web/**/*.sface"
],
theme: {
extend: {},
},
plugins: [
require("@tailwindcss/forms"),
plugin(({addVariant}) => addVariant("phx-no-feedback", ["&.phx-no-feedback", ".phx-no-feedback &"])),
plugin(({addVariant}) => addVariant("phx-click-loading", ["&.phx-click-loading", ".phx-click-loading &"])),
plugin(({addVariant}) => addVariant("phx-submit-loading", ["&.phx-submit-loading", ".phx-submit-loading &"])),
plugin(({addVariant}) => addVariant("phx-change-loading", ["&.phx-change-loading", ".phx-change-loading &"]))
]
}
We can then import the tailwind components by adding them to
assets/css/app.css
. There's a bunch of stuff there that comes with a default
Phoenix app, it's up to you if you keep it or remove it, just be sure to add
this to the top of the file:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
Styling our LiveView
First thing I did was remove the whole header
from
templates/layout/root.html.heex
and remove the class="container"
from the
main
element in templates/layout/live.html.heex
, which gives us a clean
slate to play with.
Then added the base style for our LiveView, which should cover the entire screen with a light gray background.
<div class="flex flex-col items-center justify-center bg-gray-100 h-screen">
<h1 class="text-7xl">Hello World!</h1>
</div>
Adding a switch
Next thing is adding a switch to change the theme, which can be done like this:
<div class="flex flex-col items-center justify-center bg-gray-100 h-screen">
<h1 class="text-7xl">Hello World!</h1>
<Label class="relative flex items-center cursor-pointer mt-4">
<Checkbox class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer-checked:after:translate-x-full peer-checked:after:border-white after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</Label>
</div>
All those classes create a custom toggle, which can be copy and pasted anywhere and still work, which is pretty neat.
Handling events
The next and final step for us, is to handle the event that happens when we click on the toggle. Here's how it looks both from the template and view perspective.
{#if @darkmode}
<div class="flex flex-col items-center justify-center bg-slate-800 h-screen">
<h1 class="text-7xl text-white">Hello World!</h1>
<Label class="relative flex items-center cursor-pointer mt-4">
<Checkbox click="toggle-modes" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer-checked:after:translate-x-full peer-checked:after:border-white after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</Label>
</div>
{#else}
<div class="flex flex-col items-center justify-center bg-gray-100 h-screen">
<h1 class="text-7xl">Hello World!</h1>
<Label class="relative flex items-center cursor-pointer mt-4">
<Checkbox click="toggle-modes" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer-checked:after:translate-x-full peer-checked:after:border-white after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</Label>
</div>
{/if}
defmodule SampleWeb.Pages.DarkmodeLive do
use SampleWeb, :live_view
alias Surface.Components.Form.{
Checkbox,
Label
}
data(darkmode, :boolean, default: false)
def handle_event("toggle-modes", _params, %{assigns: %{darkmode: darkmode}} = socket) do
{:noreply, assign(socket, darkmode: !darkmode)}
end
end
This works fine, but we can actually take advantage of tailwind's dark mode
support. First we need to configure it to use the class
method, so that we can
turn it on by adding a class, which we do in our tailwind.config.js
file:
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
let plugin = require("tailwindcss/plugin")
module.exports = {
...
darkmode: "class",
theme: {
extend: {},
},
...
}
And then we update the template, since the view can stay the same:
<div class={"dark": @darkmode}>
<div class="flex flex-col items-center justify-center bg-gray-100 dark:bg-slate-800 h-screen">
<h1 class="text-7xl dark:text-white">Hello World!</h1>
<Label class="relative flex items-center cursor-pointer mt-4">
<Checkbox click="toggle-modes" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer-checked:after:translate-x-full peer-checked:after:border-white after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</Label>
</div>
</div>
Note the dark:
variant to some of the utility classes. That means that they
only are in effect when there is a dark
class somewhere above them in the HTML
tree. Also, the Surface way of handling booleans that update classes.
Conclusion
This should be enough to get you going with Phoenix LiveView, Surface and Tailwind and hopefully also give you an idea of how quick it is to develop in this stack. I do recommend referring to the docs for each of these technologies as you are getting started, especially Tailwind, but the more you use it, the simpler it will be.
All the code used in the article is in a repo that you can refer to.