Phoenix Liveview - From --no-html to html & liveview
If you have generated your Phoenix app thinking you would never have to render HTML but realized later on you need to, you have landed at the right place.
This is not something easy to do since it requires carefully adding the right components at the right place.
Please note this is for Phoenix v1.6+ running with eslint.
TL-DR for anyone already comfortable with the framework:
- add the dependencies
phoenix_html
,phoenix_live_view
,phoenix_live_reload
andesbuild
- create the template folder
- create the default HTML
app.html
androot.html
templates - create your controller and your template
- in your router, define the
browser
pipeline and your routes - create your views (the glue between template & controller)
- import the standard helpers
- setup the Live Reload
- Create your JS/CSS builder
Dependencies
First thing first, let's add the following dependencies:
{:phoenix_html, "~> 3.0"},
{:phoenix_live_view, "0.16.3"},
{:phoenix_live_reload, "~> 1.3"},
{:esbuild, "0.2.1", runtime: Mix.env() == :dev},
Install the dependencies:
$ mix deps.get
Index page
Let's build a sample page and render an index page:
HTML
- In your
app_web
folder, create atemplates
folder. - In
templates
, create alayout
folder - Finally in
layout
, create aroot.html.eex
andapp.html.eex
file:
<div class="container">
<%= @inner_content %>
</div>
Controllers
Now we have our HTML, let's write the handler:
In controllers
, add your new controller (you can name the modules as you want but make sure to stick to your naming in the following steps).
I like to have an abstract base_controller
that all my other controllers can inherit from:
defmodule AppWeb.BaseController do
defmacro __using__(_) do
quote do
use AppWeb, :controller
import Phoenix.Controller
end
end
end
And then my real controller:
defmodule AppWeb.IndexController do
use AppWeb.BaseController
def index(conn, _params) do
render(conn, :index, %{})
end
end
Router
You will need a new pipeline that process queries coming from a browser:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, {AppWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
You can now add a new endpoint to test your view:
scope "/", AppWeb do
pipe_through :browser
get("/", IndexController, :index)
end
And... it doesn't work! Indeed, Phoenix needs some glue between the HTML template and the controller. And this glue is called 'view'.
Views
A view is just a module that inherits the view
behaviour of phoenix_html. By default, it provides a render
function that can be used in your controller to render the HTML.
Something important to note about views
is that Phoenix will use the module of your controller to find the path of the view to use. If your controller is named AppWeb.IndexController
the view needs to be AppWeb.IndexView
directly in views
directory. However, if you are like me and like to organize differently, if your controller is AppWeb.App.IndexController
phoenix will be looking for a module AppWeb.App.IndexView
in views/app/index.ex
Let's write a view for our index controller:
defmodule AppWeb.IndexView do
use AppWeb, :view
end
Now you can try again and... it's still not working! Indeed, the layout HTML file we previously created also needs its own view!
defmodule AppWeb.LayoutView do
use AppWeb, :view
end
And... let's try again!
HTML Helpers
Still not working amiright?
It's because we still have to include the core helpers of phoenix_html into all our views.
In app_web.ex
, edit the view_helpers
function to make it look like the following:
defp view_helpers do
quote do
use Phoenix.HTML
import Phoenix.LiveView.Helpers
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import AppWeb.ErrorHelpers
import AppWeb.Gettext
alias AppWeb.Router.Helpers, as: Routes
end
end
You can refresh and it's now all working!
Make it stylish!
We now have our HTML page rendered! That's cool but we are not in the 80s anymore. Let's add some JS and CSS to make it more stylish!
Setup
You can create a new 'assets' folder and basic files
$ mkdir assets
$ cd assets
$ mkdir js scss dist
$ touch js/app.js scss/app.scss
Make sure that you also include the basic JS code requires to make liveview run:
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
window.addEventListener("phx:page-loading-start", info => console.log('Live view loading starts'))
window.addEventListener("phx:page-loading-stop", info => console.log('Live view loading ends'))
// connect if there are any LiveViews on the page
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
Build your assets
I am using Vite
to build the assets. To do so, create a package.json that look likes the following:
{
"repository": {},
"description": " ",
"scripts": {
"build": "vite build",
"watch": "vite build --watch --minify false --emptyOutDir false --clearScreen false --mode development"
},
"dependencies": {
"@popperjs/core": "^2.10.1",
"bootstrap": "^5.1.1",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view"
},
"devDependencies": {
"sass": "^1.42.1",
"typescript": "^4.4.4",
"vite": "^2.5.10"
}
}
And create a vite.config
.js file:
export default {
publicDir: "./static",
build: {
target: "es2018",
minify: true,
outDir: "../priv/static",
emptyOutDir: true,
assetsInlineLimit: 0,
rollupOptions: {
input: ["app/styles/main.scss", "./core.js", "app/app.ts"],
output: {
entryFileNames: "dist/[name].js",
chunkFileNames: "dist/[name].js",
assetFileNames: "dist/[name][extname]"
}
},
}
}
Make sure to edit the file to make it match your folder architecture.
Live reload !
Now we have everything working well, we want to setup a live reload to refresh automatically all JS/CSS code.
First, add this to your config/dev.exs
:
config :app, AppWeb.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|scss|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/ks_web/(live|views)/.*(ex)$",
~r"lib/ks_web/templates/.*(eex)$"
]
]
Then in endpoint.ex
edit the if code_reload?
block to look like the following:
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :app
end
And with this working, you now have a working web application!
I hope this has been helpful to you. I had to figure out each piece while doing it myself. Hopefully, this made your day. Cheers !
If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.