Realizaremos una configuración básica de Surface UI, Tailwind y Sass en un proyecto Phoenix con LiveView.
Escrito por Camilo Castro y colaboradores. Para Elixir Chile.
A menos que se especifique explícitamente, los contenidos de ésta obra están bajo una Licencia Creative Commons Atribución-No-Comercial-Compartir-Igual 4.0 Internacional .
El código fuente está bajo la licencia BSD. https://github.com/elixircl/surface-tailwind-sass/
1. Creación de un nuevo proyecto
Vamos a crear un nuevo proyecto, aunque si ya tienes uno previo igual es útil, puesto que realizaremos las configuraciones de forma manual para entender como funciona todo.
Llamaremos al proyecto "miapp", como solo es una prueba no necesitaremos Ecto (base de datos) ni Mailer (envío de correos).
$ mix phx.new miapp --no-ecto --no-mailer
2. mix.exs
Vamos a nuestro archivo mix.exs
y agregamos las deps y sus configuraciones.
Primero añadir :surface
a la lista de compiladores
def project do
# [ ...
compilers: [:surface] ++ Mix.compilers(),
# ] ...
end
Añadimos la función catalogues()
para cargar los catálogos de Surface.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(:dev), do: ["lib"] ++ catalogues()
defp elixirc_paths(_), do: ["lib"]
defp catalogues do
[
"priv/catalogue",
"deps/surface/priv/catalogue"
]
end
Luego añadimos las dependencias (aprovechamos de añadir credo
igual para tener un linter).
defp deps do
# [...
{:tailwind, "~> 0.1", runtime: Mix.env() == :dev},
{:dart_sass, "~> 0.4", runtime: Mix.env() == :dev},
{:surface, "~> 0.8"},
{:surface_catalogue, "~> 0.5.1"},
{:credo, "~> 1.6"},
# ]
end
Finalmente configuramos los comandos a usar con mix
defp aliases do
[
setup: ["deps.get", "cmd npm --prefix=./assets i"],
lint: ["format", "credo --strict"],
test: ["test"],
server: ["assets.deploy", "phx.server"],
"assets.clean": ["phx.digest.clean --all"],
"assets.deploy": [
"sass default --no-source-map",
"tailwind default --minify",
"esbuild default --minify",
"phx.digest priv/static -o priv/public"
]
]
end
-
setup
: instala las dependencias de elixir y de javascript. -
lint
: formatea y evalúa que el código cumpla con los estándares de credo. -
test
: ejecuta las pruebas. -
server
: compila los assets y luego ejecuta el servidor. -
assets.clean
: limpia los archivos generados por phx.digest -
assets.deploy
: El órden es importante. Primero compila los archivosscss
, luego los combina con tailwind, sigue el compilar los archivos javascript del proyecto. Finalmente se copia los archivos generados desde el directoriostatic
al directoriopublic
.
Cuando este configurado nuestro mix.exs
podemos ejecutar mix deps.get
para instalar las dependencias.
3. .formatter.exs
Añadiremos la configuración para el comando mix format
[
# import_deps: [:ecto, :phoenix, :surface],
import_deps: [:phoenix, :surface],
plugins: [Phoenix.LiveView.HTMLFormatter, Surface.Formatter.Plugin],
inputs: ["*.{leex,heex,ex,exs,sface}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{leex,heex,ex,exs,sface}"],
subdirectories: ["priv/*/migrations"]
]
4. .gitignore
Aprovechamos de agregar algunas reglas para ignorar ciertos archivos de surface a nuestro archivo .gitignore
:
.DS_Store
_hooks/
_components.css
4.1. .vscode/settings.json
Si usas VSCode puedes añadir la siguiente configuración:
{
"scss.lint.unknownAtRules": "ignore",
"files.associations": {
"*.css": "tailwindcss"
}
}
5. Directorio assets/
Este directorio tendrá los archivos js
y css
que luego serán
procesados por tailwind
, sass
y esbuild
.
5.1. package.json
Crearemos un archivo llamado package.json
donde podremos
incluir las dependencias de javascript que necesitemos en el proyecto.
Por el momento, solamente pondremos un archivo simple sin dependencias.
{
"private": true,
"devDependencies": {
"autoprefixer": "^9.8.0"
},
"engines": {
"npm": ">=6.0.0",
"node": ">=14.0.0"
}
}
5.2. postcss.config.js
El contenido puede ser similar a lo siguiente:
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
}
Más info en https://postcss.org/
5.3. tailwind.config.js
El contenido puede ser similar a lo siguiente:
module.exports = {
important: true,
content: [
"../lib/**/*.{ex,leex,heex,eex,sface}",
"./js/_hooks/**/*.js",
"./js/app.js"
],
}
Para más detalles se puede ver la página https://tailwindcss.com/docs/configuration
5.4. css/app.scss
Cabe destacar que utilizar Sass es completamente opcional y hasta innecesario si se realiza una configuración apropiada de postcss. Revisar cómo usando acá https://tailwindcss.com/docs/using-with-preprocessors
Crearemos un archivo nuevo llamado css/app.scss
que simplemente cambia el color del background.
$color: purple;
body {
background-color: $color;
}
Aprovecharemos de eliminar los archivos:
-
app.css
-
phoenix.css
5.5. js/app.js
Vamos al archivo js/app.js
y eliminamos la importación de los estilos
css:
// import "../css/app.css"
Y agregamos los Hooks creados por Surface
import Hooks from "./_hooks"
// ...
let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks, ... })
6. Directorio config/
Vamos a usar dos configuraciones específicas. Una será la de producción
que guardará y aglomerará (digest) los archivos js y css. Guardará los archivos en el directorio priv/static
. La otra guardará en un directorio llamado
priv/public
que será usado principalmente para desarrollo (para tener autoreload) y evitar el caché.
6.1. config/config.exs
Vamos a configurar las opciones predeterminadas. Esta configuración guardará los archivos en priv/static
.
Primero añadimos que todos los assets serán entregados desde la ruta
/static
config :miapp, MiappWeb.Endpoint,
# ...
static_url: [path: "/static"]
Ahora configuramos tanto Tailwind como Sass (debajo de la config de esbuild)
# esbuild
config :esbuild,
version: "0.14.29",
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__)}
]
# sass
config :dart_sass,
version: "1.39.0",
default: [
args: ~w(--load-path=./node_modules css/app.scss ../priv/static/assets/app-raw.css),
cd: Path.expand("../assets", __DIR__)
]
# tailwind
config :tailwind,
version: "3.0.7",
default: [
args: ~w(
--config=tailwind.config.js
--input=../priv/static/assets/app-raw.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
6.2. config/dev.exs
Ésta configuración guardará los archivos en priv/public
. Sobre escribe las configuraciones y rutas de config.exs
.
config :esbuild,
version: "0.14.29",
default: [
args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/public/assets),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
],
catalogue: [
args: ~w(../deps/surface_catalogue/assets/js/app.js --bundle --target=es2017 --minify --outdir=../priv/public/assets/catalogue),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
# agregamos la configuracion de los catálogos de surface
# para que pueda encontrar los estilos, ya que modificamos
# la ruta de fábrica para los assets.
config :surface_catalogue,
assets_path: "/static/assets/catalogue/"
config :dart_sass,
version: "1.39.0",
default: [
args: ~w(--load-path=./node_modules css/app.scss ../priv/public/assets/app-raw.css),
cd: Path.expand("../assets", __DIR__)
]
config :tailwind,
version: "3.0.7",
default: [
args: ~w(
--config=tailwind.config.js
--input=../priv/public/assets/app-raw.css
--output=../priv/public/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
Luego configuramos el arreglo de watchers
para verificar cuando
se ha cambiado un archivo y volver a compilarlo. Incluyendo
los archivos javascript, sass y surface.
config :miapp, MiappWeb.Endpoint,
# ...
watchers: [
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
esbuild: {Esbuild, :install_and_run, [:catalogue, ~w(--sourcemap=inline --watch)]},
sass: {
DartSass,
:install_and_run,
[:default, ~w(--embed-source-map --source-map-urls=absolute --watch)]
},
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
]
Finalmente configuramos los formatos de archivo que serán recompilados
config :miapp, MiappWeb.Endpoint,
reloadable_compilers: [:phoenix, :elixir, :surface],
live_reload: [
patterns: [
# ...
~r"priv/public/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/catalogue/.*(ex)$",
~r"lib/miapp_web/(live|views|components)/.*(ex|js)$",
~r"lib/miapp_web/live/.*(sface)$",
# ...
]
]
7. Directorio lib/miapp_web
En este directorio irán las configuraciones de los sistemas que sirven los requests desde el navegador y renderizan html.
7.1. endpoint.ex
Necesitamos configurar el archivo endpoint.ex
para permitir
que los assets sean servidos desde nuestro directorio especial.
# ...
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# ...
plug Plug.Static,
at: "/",
from: :miapp,
gzip: false,
only: ~w(favicon.ico robots.txt)
plug Plug.Static,
at: "/static",
from: {:miapp, "priv/public"},
gzip: false,
only: ~w(assets fonts images)
7.2. router.ex
Importamos las funciones de Surface para utilizarlas en nuestras rutas.
defmodule MiappWeb.Router do
use MiappWeb, :router
import Surface.Catalogue.Router
# ...
Luego añadimos la ruta a nuestra página de index
llamada live/home.ex
.
# ...
scope "/", MiappWeb do
pipe_through :browser
live_session :default do
live "/", Live.Home, :index
end
end
# ...
Finalmente añadimos la ruta para acceder a los catálogos de Surface, solamente cuando estemos en ambiente de desarollo.
if Mix.env() == :dev do
scope "/" do
pipe_through :browser
surface_catalogue "/catalogue"
end
end
7.3. live/home.ex
Crearemos un archivo llamado live/home.ex
para renderizar un html simple usando el siguiente contenido:
defmodule MiappWeb.Live.Home do
use MiappWeb, :surface_live_view
@impl true
def render(assigns) do
~F"""
<div class={"bg-slate-100"}>
<div class={"text-sky-500"}>
<h1 class={"text-lg", "font-medium"}>Esta es Mi App</h1>
</div>
</div>
"""
end
end
7.4. miapp_web.ex
Como podemos notar estamos llamando a surface_live_view
para importar un código global. Ésto nos permitirá
simplificar el código, reutilizando la importación.
Añadimos lo siguiente a miapp_web.ex
:
def surface_live_view do
quote do
use Surface.LiveView,
layout: {MiappWeb.LayoutView, "live.html"}
unquote(view_helpers())
end
end
8. Probando
Si todo sale como esta previsto, solo bastaría ejecutar el comando
mix server
para hacer el deploy de los assets y ejecutar el servidor.
Al cual podremos acceder desde http://localhost:4000
8.1. Página Principal
8.2. Catálogo de Surface