In Jade/Pug, there are Mixin Blocks which allow you to create reusable blocks of HTML.
This mixin panel
mixin panel(title)
.panel.panel-default
if title
.panel-heading
h3.panel-title= title
.panel-body
block
+panel('Panel title')
p Panel content
would generate
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Panel title</h3>
</div>
<div class="panel-body">
<p>Panel content</p>
</div>
</div>
You can render child templates in Phoenix with render
:
<%= render "panel.html", title: "Panel title", content: "Panel content" %>
But how do you pass HTML as the content?
There are helpers in Phoenix that let you do that. For example:
<%= form_for @changeset, @action, fn f -> %>
<div class="form-group">
<%= label f, :name, class: "control-label" %>
<%= text_input f, :name, class: "form-control" %>
<%= error_tag f, :name %>
</div>
<% end %>
It looks like we can use anonymous functions!
But when you use @content()
in your panel.html.eex
, it does not seem to work!
<div class="panel panel-default">
<div class="panel-body">
<%= @content() %>
</div>
</div>
How does form_for
do it then?
def form_for(form_data, action, options \\ [], fun) when is_function(fun, 1) do
form = Phoenix.HTML.FormData.to_form(form_data, options)
html_escape [form_tag(action, form.options), fun.(form), raw("</form>")]
end
It looks like you need an extra dot to invoke anonymous functions.
<div class="panel panel-default">
<div class="panel-body">
<%= @content.() %>
</div>
</div>
Now it works.
Reusable Bootstrap helpers
Here is how to create reusable helpers/fragments/blocks/components for Bootstrap.
Create BootstrapView
in web/views/bootstrap_view.ex
.
defmodule App.BootstrapView do
use App.Web, :view
end
You can place helper functions that could be useful for generating Bootstrap components here.
Create BootstrapHelper
in web/views/helpers/bootstrap_helper.ex
.
defmodule App.BootstrapHelper do
end
Add it to web/web.ex
to make it available in all views.
defmodule App.Web do
# ...
def view do
quote do
use Phoenix.View, root: "web/templates"
# ...
import App.BootstrapHelper
end
end
# ...
end
Let's create a panel
helper for generating Bootstrap panels.
Create a template for it in web/templates/bootstrap/panel.html.eex
.
<div class="panel panel-default">
<%= if @title do %>
<div class="panel-heading">
<h3 class="panel-title"><%= @title %></h3>
</div>
<% end %>
<div class="panel-body">
<%= @content %>
</div>
<%= if @footer do %>
<div class="panel-footer">
<%= @footer %>
</div>
<% end %>
</div>
We require content
but title
and footer
are both optional.
Add the helper function in web/views/helpers/bootstrap_helper.ex
.
defmodule App.BootstrapHelper do
def panel(options \\ []) do
App.BootstrapView.render "panel.html",
title: value(options[:title]),
content: value(options[:content]),
footer: value(options[:footer])
end
defp value(fun) when is_function(fun, 0), do: fun.()
defp value(val), do: val
end
Here we are using App.BootstrapView.render
to render the template.
You don't need a dedicated view for this. You could use another existing view for this
or you could also create SharedView
and use that.
The options are a keyword list and the values can either be strings or anoymous functions with no arguments.
You can now use the panel
helper in your templates.
<%= panel title: "Panel title", content: "Panel content", footer: "Panel footer" %>
<%= panel title: "Panel title", content: fn -> "Panel content" end %>
<%= panel title: "Panel title", content: fn -> %>
<strong>Panel content</strong>
<% end %>
Bh
Luckily, there is already an open source project for this:
It's interesting to me as a beginner that you can also get do
as a parameter in your function.
Rendering an alert
<%= bh_alert context: :success, id: :one, class: :extra do %>
<b>Alert</b> is <u>very important</u>
<% end %>
is accomplished with this function
def bh_alert(opts, [do: block]) when is_list(opts) do
block
|> Bh.Service.trim_safe_text
|> bh_context_extended_alert(opts)
end
More discussion
- Reusable Templates in Phoenix by Daniel Berkompas
- Reusable Templates in Phoenix on Elixir Forum
Final remarks
I discovered Bh
when I was finishing writing this article.
But it was not a waste of time since I am using a paid admin dashboard theme (INSPINIA) that is based on Bootstrap and had to create a new set of helpers.