{"id":5709,"date":"2016-09-29T09:15:48","date_gmt":"2016-09-29T12:15:48","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=5709"},"modified":"2016-11-17T13:59:42","modified_gmt":"2016-11-17T15:59:42","slug":"dynamic-forms-with-phoenix","status":"publish","type":"post","link":"http:\/\/blog.plataformatec.com.br\/2016\/09\/dynamic-forms-with-phoenix\/","title":{"rendered":"Dynamic forms with Phoenix"},"content":{"rendered":"

Today we will learn how to build forms in Phoenix that use our schema information to dynamically show the proper input fields with validations, errors and so on. We aim to support the following API in our templates:<\/p>\n

<%= input f, :name %>\n<%= input f, :address %>\n<%= input f, :date_of_birth %>\n<%= input f, :number_of_children %>\n<%= input f, :notifications_enabled %>\n<\/code><\/pre>\n

Each generated input will have the proper markup and classes (we will use Bootstrap<\/a> in this example), include the proper HTML attributes, such as required<\/code> for required fields and validations, and show any input error.<\/p>\n

The goal is to build this foundation in our own applications in very few lines of code, without 3rd party dependencies, allowing us to customize and extend it as desired as our application changes.<\/p>\n

Setting up<\/h2>\n

Before building our input<\/code> helper, let’s first generate a new resource which we will use as a template for experimentation (if you don’t have a Phoenix application handy, run mix phoenix.new your_app<\/code> before the command below):<\/p>\n

mix phoenix.gen.html User users name address date_of_birth:datetime number_of_children:integer notifications_enabled:boolean\n<\/code><\/pre>\n

Follow the instructions after the command above runs and then open the form template at “web\/templates\/user\/form.html.eex”. We should see a list of inputs such as:<\/p>\n

<div class=\"form-group\">\n  <%= label f, :address, class: \"control-label\" %>\n  <%= text_input f, :address, class: \"form-control\" %>\n  <%= error_tag f, :address %>\n<\/div>\n<\/code><\/pre>\n

The goal is to replace each group above by a single <%= input f, field %><\/code> line.<\/p>\n

Adding changeset validations<\/h3>\n

Still in the “form.html.eex” template, we can see that a Phoenix form operates on Ecto changesets:<\/p>\n

<%= form_for @changeset, @action, fn f -> %>\n<\/code><\/pre>\n

Therefore, if we want to automatically show validations in our forms, the first step is to declare those validations in our changeset. Open up “web\/models\/user.ex” and let’s add a couple new validations at the end of the changeset<\/code> function:<\/p>\n

|> validate_length(:address, min: 3)\n|> validate_number(:number_of_children, greater_than_or_equal_to: 0)\n<\/code><\/pre>\n

Also, before we do any changes to our form, let’s start the server with mix phoenix.server<\/code> and access http:\/\/localhost:4000\/users\/new<\/code> to see the default form at work.<\/p>\n

Writing the input<\/code> function<\/h2>\n

Now that we have set up the codebase, we are ready to implement the input<\/code> function.<\/p>\n

The YourApp.InputHelpers<\/code> module<\/h3>\n

Our input<\/code> function will be defined in a module named YourApp.InputHelpers<\/code> (where YourApp<\/code> is the name of your application) which we will place in a new file at “web\/views\/input_helpers.ex”. Let’s define it:<\/p>\n

defmodule YourApp.InputHelpers do\n  use Phoenix.HTML\n\n  def input(form, field) do\n    \"Not yet implemented\"\n  end\nend\n<\/code><\/pre>\n

Note we used Phoenix.HTML<\/code> at the top of the module to import the functions from the Phoenix.HTML<\/code> project<\/a>. We will rely on those functions to build the markup later on.<\/p>\n

If we want our input<\/code> function to be automatically available in all views, we need to explicitly add it to the list of imports in the “def view” section of our “web\/web.ex” file:<\/p>\n

import YourApp.Router.Helpers\nimport YourApp.ErrorHelpers\nimport YourApp.InputHelpers # Let's add this one\nimport YourApp.Gettext\n<\/code><\/pre>\n

With the module defined and properly imported, let’s change our “form.html.eex” function to use the new input<\/code> functions. Instead of 5 “form-group” divs:<\/p>\n

<div class=\"form-group\">\n  <%= label f, :address, class: \"control-label\" %>\n  <%= text_input f, :address, class: \"form-control\" %>\n  <%= error_tag f, :address %>\n<\/div>\n<\/code><\/pre>\n

We should have 5 input calls:<\/p>\n

<%= input f, :name %>\n<%= input f, :address %>\n<%= input f, :date_of_birth %>\n<%= input f, :number_of_children %>\n<%= input f, :notifications_enabled %>\n<\/code><\/pre>\n

Phoenix live-reload should automatically reload the page and we should see “Not yet implemented” appear 5 times.<\/p>\n

Showing the input<\/h3>\n

The first functionality we will implement is to render the proper inputs as before. To do so, we will use the Phoenix.HTML.Form.input_type<\/code> function<\/a>, that receives a form and a field name and returns which input type we should use. For example, for :name<\/code>, it will return :text_input<\/code>. For :date_of_birth<\/code>, it will yield :datetime_select<\/code>. We can use the returned atom to dispatch to Phoenix.HTML.Form<\/code> and build our input:<\/p>\n

def input(form, field) do\n  type = Phoenix.HTML.Form.input_type(form, field)\n  apply(Phoenix.HTML.Form, type, [form, field])\nend\n<\/code><\/pre>\n

Save the file and watch the inputs appear on the page!<\/p>\n

Wrappers, labels and errors<\/h3>\n

Now let’s take the next step and show the label and error messages, all wrapped in a div:<\/p>\n

def input(form, field) do\n  type = Phoenix.HTML.Form.input_type(form, field)\n\n  content_tag :div do\n    label = label(form, field, humanize(field))\n    input = apply(Phoenix.HTML.Form, type, [form, field])\n    error = YourApp.ErrorHelpers.error_tag(form, field) || \"\"\n    [label, input, error]\n  end\nend\n<\/code><\/pre>\n

We used content_tag<\/code> to build the wrapping div<\/code> and the existing YourApp.ErrorHelpers.error_tag<\/code> function that Phoenix generates for every new application that builds an error tag with proper markup.<\/p>\n

Adding Bootstrap classes<\/h3>\n

Finally, let’s add some HTML classes to mirror the generated Bootstrap markup:<\/p>\n

def input(form, field) do\n  type = Phoenix.HTML.Form.input_type(form, field)\n\n  wrapper_opts = [class: \"form-group\"]\n  label_opts = [class: \"control-label\"]\n  input_opts = [class: \"form-control\"]\n\n  content_tag :div, wrapper_opts do\n    label = label(form, field, humanize(field), label_opts)\n    input = apply(Phoenix.HTML.Form, type, [form, field, input_opts])\n    error = YourApp.ErrorHelpers.error_tag(form, field)\n    [label, input, error || \"\"]\n  end\nend\n<\/code><\/pre>\n

And that’s it! We are now generating the same markup that Phoenix originally generated. All in 14 lines of code. But we are not done yet, let’s take things to the next level by further customizing our input function.<\/p>\n

Customizing inputs<\/h2>\n

Now that we have achieved parity with the markup code that Phoenix generates, we can further extend it and customize it according to our application needs.<\/p>\n

Colorized wrapper<\/h3>\n

One useful UX improvement is to, if a form has errors, automatically wrap each field in a success or error state accordingly. Let’s rewrite the wrapper_opts<\/code> to the following:<\/p>\n

wrapper_opts = [class: \"form-group #{state_class(form, field)}\"]\n<\/code><\/pre>\n

And define the private state_class<\/code> function as follows:<\/p>\n

defp state_class(form, field) do\n  cond do\n    # The form was not yet submitted\n    !form.source.action -> \"\"\n    form.errors[field] -> \"has-error\"\n    true -> \"has-success\"\n  end\nend\n<\/code><\/pre>\n

Now submit the form with errors and you should see every label and input wrapped in green (in case of success) or red (in case of input error).<\/p>\n

Input validations<\/h3>\n

We can use the Phoenix.HTML.Form.input_validations<\/code> function<\/a> to retrieve the validations in our changesets as input attributes and then merge it into our input_opts<\/code>. Add the following two lines after the input_opts<\/code> variable is defined (and before the content_tag<\/code> call):<\/p>\n

validations = Phoenix.HTML.Form.input_validations(form, field)\ninput_opts = Keyword.merge(validations, input_opts)\n<\/code><\/pre>\n

After the changes above, if you attempt to submit the form without filling the “Address” field, which we imposed a length of 3 characters, the browser won’t allow the form to be submitted. Not everyone is a fan of browser validations and, in this case, you have direct control if you want to include them or not.<\/p>\n

At this point it is worth mentioning both Phoenix.HTML.Form.input_type<\/code> and Phoenix.HTML.Form.input_validations<\/code> are defined as part of the Phoenix.HTML.FormData<\/code> protocol. This means if you decide to use something else besides Ecto changesets to cast and validate incoming data, all of the functionality we have built so far will still work. For those interested in learning more, I recommend checking out the Phoenix.Ecto<\/code> project<\/a> and learn how the integration between Ecto and Phoenix is done by simply implementing protocols exposed by Phoenix.<\/p>\n

Per input options<\/h3>\n

The last change we will add to our input<\/code> function is the ability to pass options per input. For example, for a given input, we may not want to use the type inflected by input_type<\/code>. We can add options to handle those cases:<\/p>\n

def input(form, field, opts \\\\ []) do\n  type = opts[:using] || Phoenix.HTML.Form.input_type(form, field)\n<\/code><\/pre>\n

This means we can now control which function to use from Phoenix.HTML.Form<\/code> to build our input:<\/p>\n

<%= input f, :new_password, using: :password_input %>\n<\/code><\/pre>\n

We also don’t need to be restricted to the inputs supported by Phoenix.HTML.Form<\/code>. For example, if you want to replace the :datetime_select<\/code> input that ships with Phoenix by a custom datepicker, you can wrap the input creation into an function and pattern match on the inputs you want to customize.<\/p>\n

Let’s see how our input<\/code> functions look like with all the features so far, including support for custom inputs (input validations have been left out):<\/p>\n

defmodule YourApp.InputHelpers do\n  use Phoenix.HTML\n\n  def input(form, field, opts \\\\ []) do\n    type = opts[:using] || Phoenix.HTML.Form.input_type(form, field)\n\n    wrapper_opts = [class: \"form-group #{state_class(form, field)}\"]\n    label_opts = [class: \"control-label\"]\n    input_opts = [class: \"form-control\"]\n\n    content_tag :div, wrapper_opts do\n      label = label(form, field, humanize(field), label_opts)\n      input = input(type, form, field, input_opts)\n      error = YourApp.ErrorHelpers.error_tag(form, field)\n      [label, input, error || \"\"]\n    end\n  end\n\n  defp state_class(form, field) do\n    cond do\n      # The form was not yet submitted\n      !form.source.action -> \"\"\n      form.errors[field] -> \"has-error\"\n      true -> \"has-success\"\n    end\n  end\n\n  # Implement clauses below for custom inputs.\n  # defp input(:datepicker, form, field, input_opts) do\n  #   raise \"not yet implemented\"\n  # end\n\n  defp input(type, form, field, input_opts) do\n    apply(Phoenix.HTML.Form, type, [form, field, input_opts])\n  end\nend\n<\/code><\/pre>\n

And then, once you implement your own :datepicker<\/code>, just add to your template:<\/p>\n

<%= input f, :date_of_birth, using: :datepicker %>\n<\/code><\/pre>\n

Since your application owns the code, you will always have control over the inputs types and how they are customized. Luckily Phoenix ships with enough functionality to give us a head start, without compromising our ability to refine our presentation layer later on.<\/p>\n

Summing up<\/h2>\n

This article showed how we can leverage the conveniences exposed in Phoenix.HTML<\/code> to dynamically build forms using the information we have already specified in our schemas. Although the example above used the User schema, which directly maps to a database table, Ecto 2.0 allows us to use schemas to map to any data source<\/a>, so the input<\/code> function can be used for validating search forms, login pages, and so on without changes.<\/p>\n

While we have developed projects such as Simple Form<\/a> to tackle those problems in our Rails projects, with Phoenix we can get really far using the minimal abstractions that ship as part of the framework, allowing us to get most of the functionality while having full control over the generated markup.<\/p>\n

\n\"Elixir<\/a>\n<\/div>\n","protected":false},"excerpt":{"rendered":"

Today we will learn how to build forms in Phoenix that use our schema information to dynamically show the proper input fields with validations, errors and so on. We aim to support the following API in our templates: <%= input f, :name %> <%= input f, :address %> <%= input f, :date_of_birth %> <%= input … \u00bb<\/a><\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[1],"tags":[143,245],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5709"}],"collection":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=5709"}],"version-history":[{"count":8,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5709\/revisions"}],"predecessor-version":[{"id":5888,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5709\/revisions\/5888"}],"wp:attachment":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=5709"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=5709"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=5709"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}