peteris.rocks

Helpers in Phoenix Framework with Elixir

How to create reusable Bootstrap helpers in Phoenix Framework

Last updated on

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

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.