How to create reusable Bootstrap helpers in Phoenix Framework

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.