Skip to content

Instantly share code, notes, and snippets.

@abitdodgy
Created April 27, 2020 22:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abitdodgy/cbe56b802725a46bf682001a38873b7b to your computer and use it in GitHub Desktop.
Save abitdodgy/cbe56b802725a46bf682001a38873b7b to your computer and use it in GitHub Desktop.
ExBs Form
defmodule ExBs.Components.Form do
@moduledoc """
Helpers for building Phoenix forms with Bootstrap components.
"""
import ExBs.Config, only: [translation_fn: 0]
import ExBs.Components.Form.Input, only: [input: 3]
alias Phoenix.HTML.{Form, Tag}
@config ExBs.Config.Bootstrap.get_config(:form)
defp get_class(key), do: @config[:classes][key]
defp get_option(key), do: @config[key]
@doc """
Builds a Boostrap 4 form group with a label, input, an errors and help text components.
## Form Group
The `form_group` option allows forwarding its values to the form_group element tag. It
must be a keyword list, accepting `:tag` key to built HTML tag. All other options will
be passed to HTML tag like attributes.
### Examples
Generates a default form group.
form_group(f, :age)
#=> <div class="form-group">
<label for="user_age">Age</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
</div>
Generates a form group with an additional options.
form_group(f, :age, form_group: [tag: :span, class: "custom"])
#=> <span class="form-group custom">
<label for="user_age">Age</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
</span>
## Label
The `label` option generates an HTML label for the input. It can be a keyword
list, a string, `false`, or `nil`. When `nil` or omitted, the label is
inflected from the field name.
To generate a form_group without a label, use `label: false`. To create a label
with a custom text, pass in a string.
* form_group(form, :name, label: false)
* form_group(form, :name, label: "Custom")
You can also pass a keyword list of attributes that will be forwarded onto the html.
The same label rules above apply when using a keyword list except for custom text.
* form_group(form, :name, label: [class: "my-class"])
To customise the label text when using a keyword list, use the key `text` with a string.
* form_group(form, :name, label: [text: "Custom"])
## Required Field Marker
A configurable required marker, `*`, is appended to labels of required fields.
Customise this marker by setting the `:required_field_mark` configuration option. The value
is ran through the translation function.
* config :ex_bs, :required_field_mark, "Custom"
Disable this behaviour by setting this option to `false`.
* config :ex_bs, :required_field_mark, false
### Examples
Generates a form group without a label.
form_group(f, :age, label: false)
#=> <div class="form-group">
<input class="form-control " id="user_age" name="user[age]" type="number">
</div>
Generates a form group a default label.
form_group(f, :age)
#=> <div class="form-group">
<label for="user_age">Age</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
</div>
Generates a form group with a custom label.
form_group(f, :age, label: "Your age?")
#=> <div class="form-group">
<label for="user_age">Your age?</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
</div>
Generates a form group with a label and additional options.
form_group(f, :age, label: [text: "Your age?", class: "custom"])
#=> <div class="form-group">
<label class="custom" for="user_age">Your age?</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
</div>
Generates a form group with a label for a required field.
form_group(f, :name)
#=> <div class="form-group">
<label for="user_name">Name<span> *</span></label>
<input class="form-control " id="user_name" name="user[name]" type="text">
</div>
## Input
See `input/3` for details of input builder. To generate a form_group with with a
custom input, pass option `:do` with a phoenix component.
### Examples
Generates a form group with custom input.
form_group f, :age do
select f, :age, 16..22, class: "custom-select"
end
#=> <div class="form-group">
<label for="user_age">Age</label>
<select class="custom-select" id="user_age" name="user[age]">
<option value="16">16</option>
<option value="17">17</option>
<option value="18" selected>18</option>
<option value="19">19</option>
<option value="20">20</option>
<option value="21">21</option>
<option value="22">22</option>
</select>
</div>
## Prepend / Append
Use the `prepend` and `append` options to preppend of append text to an input.
### Examples
form_group(f, :age, prepend: "Years")
#=> <div class="form-group">
<label for="user_age">Age</label>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Years</div>
</div>
<input class="form-control " id="user_age" name="user[age]" type="number">
</div>
</div>
## Errors
Errors are rendered under the input and are wrapped in a customisable error class.
## Help Text
The `help` option builds an HTML tag containing help text. It can be a string
or a keyword list.
* form_group(form, :name, help: "A hint...")
When using a keyword list, pass the help text as a string to the `text` option. Other
attributes will be forwarded onto the html of the help tag.
* form_group(form, :name, help: [text: "A hint...", class: "my-class"])
Customise the html tag using the `tag` option.
* form_group(form, :name, help: [text: "A hint...", tag: :div])
### Examples
Generates a form group with a help text.
form_group(f, :age, help: "Some help")
#=> <div class="form-group">
<label for="user_age">Age</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
<small class="form-text text-muted">Some help</small>
</div>
Generates a form group with a help text and additional options.
form_group(f, :age, help: [text: "Some help", class: "custom"])
#=> <div class="form-group">
<label for="user_age">Age</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
<small class="custom">Some help</small>
</div>
Generates a form group with help using a custom html tag.
form_group(f, :age, help: [text: "Some help", tag: :div])
#=> <div class="form-group">
<label for="user_age">Age</label>
<input class="form-control " id="user_age" name="user[age]" type="number">
<div>Some help</div>
</div>
## Other Options
* Other given options are forwarded to the input tag.
"""
def form_group(form, field), do: form_group(form, field, [], do: nil)
def form_group(form, field, do: block), do: form_group(form, field, [], do: block)
def form_group(form, field, opts), do: form_group(form, field, opts, do: nil)
def form_group(form, field, opts, do: do_block) do
{form_group_opts, opts} = Keyword.pop(opts, :form_group, [])
%{form: form, field: field, opts: opts, safe: [], do: do_block}
|> draw_help()
|> input_or_group_with_errors()
|> draw_label()
|> Map.get(:safe)
|> draw_form_group(form_group_opts)
end
@doc """
Creates a help text component. Accepts a keyword list of attributes
that is forwarded onto the html.
## Examples
form_text("Pro-tip")
#=> <div class="form-text text-muted">Pro-tip</div>
form_text("Pro-tip", class: "my-class")
#=> <div class="my-class">Pro-tip</div>
"""
def form_text(text, opts \\ []) do
default_tag = get_option(:form_text_tag)
{tag, opts} = Keyword.pop(opts, :tag, default_tag)
Tag.content_tag(tag, text, Keyword.merge([class: get_class(:form_text)], opts))
end
@doc """
Creates an input group component. Accepts a keyword list of attributes
that is forwarded onto the html.
Use the `prepend` and `append` options to create group prepend and
append components.
## Examples
input_group do
input(f, :age)
end
#=> <div class="input-group">
<input class="form-control " id="user_age" name="user[age]" type="number">
</div>
input_group prepend: "@" do
"Foo"
end
#=> <div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">@</div>
</div>
Foo
</div>
"""
def input_group(do: block), do: input_group([], do: block)
def input_group(opts, do: block) when is_list(opts) do
siblings = Keyword.take(opts, [:prepend, :append])
opts =
[class: get_class(:input_group)]
|> Keyword.merge(opts)
|> Keyword.drop([:prepend, :append])
Tag.content_tag :div, opts do
content_for_input_group(siblings, block)
end
end
defp content_for_input_group([], block), do: block
defp content_for_input_group([prepend: prepend], block) do
[input_group_prepend(prepend), block]
end
defp content_for_input_group([append: append], block) do
[block, input_group_append(append)]
end
defp content_for_input_group([prepend: prepend, append: append], block) do
[input_group_prepend(prepend), block, input_group_append(append)]
end
@doc """
Creates an input group prepend component.
## Examples
input_group_prepend("@")
#=> <div class="input-group-prepend">
<div class="input-group-text">@</div>
</div>
"""
def input_group_prepend(text) do
Tag.content_tag :div, class: get_class(:input_group_prepend) do
Tag.content_tag(:div, text, class: get_class(:input_group_text))
end
end
@doc """
Creates an input group append component.
## Examples
input_group_append("@")
#=> <div class="input-group-append">
<div class="input-group-text">@</div>
</div>
"""
def input_group_append(text) do
Tag.content_tag :div, class: get_class(:input_group_append) do
Tag.content_tag(:div, text, class: get_class(:input_group_text))
end
end
defp draw_label(%{form: form, field: field, opts: opts, safe: safe} = data) do
case Keyword.get(opts, :label) do
false ->
data
label ->
Map.put(data, :safe, [label(form, field, label) | safe])
end
end
defp label(form, field, nil) do
Form.label form, field do
label_text(form, field, Form.humanize(field))
end
end
defp label(form, field, text) when is_binary(text) do
Form.label form, field do
label_text(form, field, text)
end
end
defp label(form, field, opts) when is_list(opts) do
{label, opts} = Keyword.pop(opts, :text, Form.humanize(field))
Form.label form, field, opts do
label_text(form, field, label)
end
end
defp label_text(form, field, label) do
translated_label = translation_fn().(label)
if required_field?(form, field) and mark_required_fields?() do
[translated_label, draw_required_field_marker()]
else
translated_label
end
end
defp mark_required_fields?, do: get_option(:mark_required_fields)
defp required_field?(form, field) do
required =
form
|> Form.input_validations(field)
|> Keyword.get(:required)
!!required
end
defp required_field_marker do
get_option(:required_field_marker)
end
defp draw_required_field_marker do
mark =
required_field_marker()
|> translation_fn().()
Tag.content_tag(:span, [" ", mark], class: get_class(:required_field_marker))
end
defp input_or_group_with_errors(
%{form: form, field: field, opts: opts, safe: safe, do: do_block} = data
) do
input = do_block || input(form, field, opts)
content = [input | errors(form, field)]
case Keyword.take(opts, [:prepend, :append]) do
[] ->
Map.put(data, :safe, [content | safe])
siblings ->
Map.put(data, :safe, [input_group(siblings, do: content) | safe])
end
end
defp errors(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn {msg, _opts} ->
Tag.content_tag(:div, translation_fn().(msg), class: get_class(:error_message))
end)
end
defp draw_help(%{opts: opts, safe: safe} = data) do
if help = Keyword.get(opts, :help) do
Map.put(data, :safe, [draw_help(help) | safe])
else
data
end
end
defp draw_help(text) when is_binary(text), do: form_text(text)
defp draw_help(opts) when is_list(opts) do
case Keyword.pop(opts, :text) do
{nil, _opts} ->
raise KeyError, "`help` options must include a `text` option"
{text, opts} ->
form_text(text, opts)
end
end
defp draw_form_group(io_data, opts) do
{tag, opts} = Keyword.pop(opts, :tag, :div)
opts =
[class: get_class(:form_group)]
|> Keyword.merge(opts, fn
:class, current_value, new_value ->
"#{current_value} #{new_value}"
_key, _current_value, new_value ->
new_value
end)
Tag.content_tag(tag, io_data, opts)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment