HanamiMastery

Advanced forms in Hanami 2

Episode #53

by Sebastian Wilgosz

Picture of the author

Hi there!

In the previous episode, I made the introduction to using scopes in Hanami 2 applications. Today I will dig a bit further, and leverage the Parts feature, so if you're not familiar with this, I'd recommend visiting the guides first or checking out my previous episodes on these topics.

Today I want to tackle the logic extraction from complex forms in Hanami 2, step-by-step, and because of that it can take a while, so please prepare yourself for a longer episode.

Overview of the problem

I have here the registration form I've created for rendering the flash messages tutorial, however, it's not fully functional yet.

Basic form error with no errorsBasic form error with no errors

It contains a couple of fields only, but you may already see that it's a pretty bloated piece of HTML, that is supposed to mix quite a bunch of rendering logic in.

<%= form_for :registration, routes.path(:register_account) do |f| %>
  <div class="field">
    <%= f.label "username", class: "label" %>
    <div class="control has-icons-left">
      <%= f.text_field :username, class: "input" %>
      <span class="icon is-small is-left">
        <i class="fas fa-user"></i>
      </span>
    </div>
  </div>

  <div class="field">
    <%= f.label "password", class: "label" %>
    <div class="control has-icons-left">
      <%= f.password_field :password, class: "input" %>
      <span class="icon is-small is-left">
        <i class="fas fa-lock"></i>
      </span>
    </div>
  </div>

  <div class="field">
    <%= f.label "password_confirmation", class: "label" %>
    <div class="control has-icons-left">
      <%= f.password_field :password_confirmation, class: "input" %>
      <span class="icon is-small is-left">
        <i class="fas fa-lock"></i>
      </span>
    </div>
  </div>

  <div class="field">
    <div class="control">
      <label class="checkbox">
        <input type="checkbox">
        I agree to the <a href="#">terms and conditions</a>
      </label>
    </div>
  </div>

  <div class="field is-grouped">
    <div class="control">
      <button class="button is-link">Submit</button>
    </div>
    <div class="control">
      <button class="button is-link is-light">Cancel</button>
    </div>
  </div>
<% end %>

For example, if there are errors in a particular field, the input should be highlighted with a red border, and the exclamation icon should be added to the right side of the input.

Additionally, below the input, there should be the exact red notification about what's wrong with this field.

Registration form with error messagesRegistration form with error messages

Today I want to do the challenge to extract this logic and refactor the code, so it's easier to work with the dynamic data we have here.

Let's start then.

Render the field input error messages

Rendering errors from the action.

Before we do anything, let's make sure we can properly render the field errors. For that purpose, I'm going to visit my create action and add the custom view rendering line there.

By default, the create action will render the new view, with the request parameters being accessible, so the form can preserve the field values we passed in. Except password fields of course, which are filtered out.

However, because we want to add the errors to work with, let me render the view manually.

# slices/main/actions/registrations/create.rb

else
  response.flash.now[:alert] = "Error occured while creating the account"
  response.render(
    view,
    values: request.params[:registration],
    errors: request.params.errors[:registration]
  )
end

Expose the form object in the view

Now in the view, I can make use of this to expose the form object I would like to work with in the templates. I'll make it dependent on the values and errors input arguments, but will set defaults for them, so the new action can render the view without passing any parameters to it.

Then inside, I'll return the hash that merges those two together and will decorate this value in a moment.

By default, to decorate this, the Parts::Form class would be used, but because this is the only form in my app right now, and the common patterns haven't emerged yet, I'll make this more explicit, and paste the custom part class in the options.

# slices/main/views/registrations/new

module Main
  module Views
    module Registrations
      class New < Main::View
        expose(:form, as: Parts::Forms::Registration) do |values: {}, errors: {}|
          {
            values: values,
            errors: errors
          }
        end
      end
    end
  end
end

Building a registration form part

Now let's build this part. I'm going to create a new part file, and inside add the errors method, that allows me to list the error messages for a particular input field, so I'll be able to list those below the form's entry.

# slices/main/views/parts/forms/registration.rb

module Main
  module Views
    module Parts
      module Forms
        class Registration < Part
          def errors(key)
            value.dig(:errors, key).to_a
          end
        end
      end
    end
  end
end

Rendering input field error messages

That's enough for now. Let's use it in the template now.

Under each field, I'm going to paste the line that will render the field error messages, and paste that messages as a local. Then I only want to render errors if there is actually an error message.

<div class="field">
  <%= f.label "username", class: "label" %>
  <div class="control has-icons-left">
    <%= f.text_field :username, class: "input" %>
    <span class="icon is-small is-left">
      <i class="fas fa-user"></i>
    </span>
    <%= render("shared/forms/field_errors", messages: forms.error(:username)) if form.errors(:username).any? %>
  </div>
</div>

I also need to add dynamic class names for the form if the field errors out

<div class="control has-icons-left <%= 'has-icons-right is-danger' if form.errors[:username].any? %>">
Disclaimer

By the way, I know there is a logic here, I am doing things step by step so you won't get lost in my thinking process.

When I copy these field implementations over, you'll see that the only thing that changes here is the field name, and that's already a pattern for me.

Then I will add the _field_errors partial. First I am going to render the exclamation triangle icon span, and then for each of the provided messages, I'll add the properly styled paragraph one under the other.

# slices/main/templates/shared/forms/_field_errors.html.erb

<span class="icon is-small is-right">
  <i class="fas fa-exclamation-triangle"></i>
</span>

<% messages.each do |msg| %>
  <p class="help is-danger"><%= msg %></p>
<% end %>

The form now should render errors for each field. Voila! That's the basic level. Now let's go to the level 2.

Form input field logic extraction

All those fields are very similar. The HTML part is different, but the logic inside is just the same. All they need is

  • a form object to render the field values correctly
  • information if the error is there,
  • error messages,
  • icon name
  • attribute name
  • and maybe custom label text as you may see in the case of the checkbox here.

Step 1. Normalize your partials

Let me then extract the fields to separate field-based templates. I'm going to create a separate template for each field type in the shared/forms directory and paste the code inside, simplifying the logic a bit.

First I want to create the text_field template. I want to remove the username and replace it with the general field_name local. Then I want to be able to pass down a custom label text if needed.

# slices/main/templates/shared/forms/_text_field.html.erb

<%- 
field_name = 'username'
label_text = 'Username'
%>
<div class="field">
  <%= (f.label field_name, class: "label") { label_text } %>
  <div class="control has-icons-left">
    <%= f.text_field field_name, class: "input" %>
    <span class="icon is-small is-left">
      <i class="fas fa-user"></i>
    </span>
    <%= render("shared/forms/field_errors", messages: forms.error[:username]) if form.errors[:username].any? %>
  </div>
</div>

The icon here should also not be hardcoded, but rather set dynamically.

# slices/main/templates/shared/forms/_form_icon.html.erb
<%- icon_name = 'user' %>

<span class="icon is-small is-left">
  <i class="fas fa-<%= icon_name %>"></i>
</span>

Now let's look at error checks. There are two places that check if the field is an error, so let's extract this out too. It comes with setting up dynamic set of classes for the control field that can be extracted to a variable together.

<%
error_field? = form.errors[:username].any?
control_class_names = ''
control_class_names << 'has-icons-right' if error_field?
%>
<div class="field">
  <%= (f.label field_name, class: "label") { label_text } %>
  <div class="control has-icons-left <%= control_class_names %>">
    <%= f.text_field field_name, class: "input" %>
    <span class="icon is-small is-left">
      <i class="fas fa-user"></i>
    </span>
    <%= render("shared/forms/field_errors", messages: forms.error[:username]) if errors_field? %>
  </div>
</div>

Next, let's tackle the error messages, to not be dependent on the field name at all.

<%- errors = forms.error[:username] %>
<div class="field">
  <%= (f.label field_name, class: "label") { label_text } %>
  <div class="control has-icons-left <%= control_class_names %>">
    <%= f.text_field field_name, class: "input" %>
    <span class="icon is-small is-left">
      <i class="fas fa-user"></i>
    </span>
    <%= render("shared/forms/field_errors", messages: errors if errors_field? %>
  </div>
</div>

You may notice here, that the if statement is left here to render the errors partial, and I don't know how about you, but for me, it's logic in a template for sure! However, I've already shown the other approach to render field-specific errors in forms in [[46-contact-form|episode 46]], so you may as well check that out, and here I'll just leave it like this.

Finally, let's take a closer look at the class names. There are custom classes added, if there is an error, but also, if the icon is set. Without the icon, we don't need the has-icons-left class, which makes this list of classes even more complex. Let's extract this out too.

<%
has_icon? = !icon_name.nil?
control_class_names = ''
control_class_names << 'has-icons-left' if has_icon?
control_class_names << 'has-icons-right' if error_field?
%>
<div class="field">
  <%= (f.label field_name, class: "label") { label_text } %>
  <div class="control has-icons-left <%= control_class_names %>">
    <%= f.text_field field_name, class: "input" %>
    <span class="icon is-small is-left">
      <i class="fas fa-user"></i>
    </span>
    <%= render("shared/forms/field_errors", messages: errors) if errors_field? %>
  </div>
</div>

The span class should also be conditional based on the fact that the icon is present. Already we've discovered a bug in our code that can be fixed in a moment! Isn't that cool?

Let me then extract the icon span into a separate partial. Let's render the icon passing down the name as a local, and the aligment information. Then do nothing if the icon should not be set.

I'll create a new partial, named field_icon, and inside I'll paste my span, replacing dynamic content with local variables. The icon_name local I'll rename to icon, and replace that when rendering too.

# slices/main/templates/shared/forms/field_icon.rb

<span class="icon is-small is-<%= align %>">
  <i class="fas fa-<%= icon %>"></i>
</span>

I can replace that when rendering the partial too!

has_icon? = !icon_name.nil?
<div class="field">
  <%= (f.label field_name, class: "label") { label_text } %>
  <div class="control has-icons-left <%= 'has-icons-right' if error_field? %>">
    <%= f.text_field field_name, class: "input" %>
    <%= render("shared/forms/field_icon", icon: 'user', align: 'left') %>
    <%= render("shared/forms/field_errors", messages: errors if errors_field? %>
  </div>
</div>

I can even go to the field_errors template now and also change the hardcoded span tag to just render the icon partial.

# slices/main/templates/shared/forms/_field_errors.html.erb
<%= render("shared/forms/_field_icon", icon_name: 'exclamation-triangle', align: 'right') %>

<% messages.each do |msg| %>
  <p class="help is-danger"><%= msg %></p>
<% end %>

Oh! I see I've named the icon partial wrongly. Let me quickly rename the file to field_icon, and reflect that in partials that call it. Now it's fine.

Finally, let's take a closer look at the input field. I want to have the extra class added in case of an error, so it gets the ==red highlight==. I will extract it the same way as I did for a wrapper div.

input_field_classes = "input #{ 'is-danger' if error_field? }"

When I think about it, I can even add a dynamic placeholder support. Let's also allow this to be passed in.

Pheeew, done. That's crazy! I hope you can see now, how much logic is hidden in templates of most applications, and with this knowledge, we can together make the world a better place!

<%
field_name = 'username'
label_text = 'Username'
icon_name = 'user'
has_icon? = !icon_name.nil?
error_field? = form.errors[:username].any?
errors = forms.error[:username]
control_class_names = ''
control_class_names << ' has-icons-left' if has_icon?
control_class_names << ' has-icons-right is-danger' if error_field?
placeholder_text = ''
input_field_classes = "input #{ 'is-danger' if error_field? }"
%>
<div class="field">
  <%= f.label(field_name, class: "label") { label_text } %>
  <div class="control <%= control_class_names %>">
    <%= f.text_field(field_name, class: input_field_classes, placeholder: placeholder_text) %>
    <%= render('field_icon', icon: icon_name) if has_icon? %>
    
    <%= render("field_errors", messages: errors) if error_field? %>
  </div>
</div>

I know this is a lot, but by extracting this we can copy the partial and easily adjust it to a password field, email field, or anything else. I will quickly write down the checkbox field here yet, to verify if I need any other dynamic data.

<div class="field">
  <div class="control">
    <%= f.check_box field_name, checked_value: "true", unchecked_value: "false" %>
    <%= f.label class: "checkbox" do %>
      <%= label_text %>
    <% end %>

    <%= render("field_errors", messages: errors) if error_field? %>
  </div>
</div>

No, it seems I need even fewer methods.

Cool! So the next step for me is to write the form input scope, that will accept different inputs and resolve the local variables in a way understandable for an input field partial.

Step 2. Prepare the Form Input Scope

In the new scope file, I am going to paste down all the logic I have extracted from my partial and then wrap them up with the nice ruby methods.

field_name will just call super, and stringify the value, so we can pass in symbols as well. label_text should be optional, so if there is no label attribute passed in, we'll return the humanized field_name.

let's skip the icon_name for now... to decide whether the field is error, we need to check errors array, and we already ensure this will be an array, so let's write this down.

Next we have control_class_names. First I need to have the default value, add the icon class if error has icon, and error, if it has an error. At the end we join the classes into a single string. This means, that in the test_field partial I can remove the first part of the control classes and just rely on the variable.

# frozen_string_literal: true

module Main
  module Views
    module Scopes
      module Forms
        class Input < Main::Views::Scope
          def field_name
            super.to_s
          end

          def label_text
            return label if respond_to?(:label) && label

            _context.inflector.humanize(field_name)
          end

          def control_field_classes
            classes = %w[control has-icons-left]
            classes << 'has-icons-right' if error_field?
            classes.join(' ')
          end

          def placeholder_text
            return placeholder if respond_to?(:placeholder) && placeholder

            _context.inflector.humanize(field_name)
          end

          def input_field_classes
            classes = %w[input]
            classes << "is-danger" if error_field?
            classes.join(' ')
          end

          def error_field?
            errors.any?
          end
        end
      end
    end
  end
end

Going back to the scope, errors will be just passed as arguments, so we don't need it. After that I imlpement the input_field_classes method similar to what we have in the control tag, and for the placeholder, I'll again, return the passed in argument, or fallback to default calculated on the field name.

Ok. We have now an amazing class, that we can unit test and that can guarantee that our partials will be rendered correctly. There is a big caveat here though.

If I go to the form template, replace my inputs by calling the input scope with some attributes, and then render the exact template, it does not look much better, does it?

scope(Forms::Input).render(
  field_name: 'username',
  errors: form.errors,
  placeholder: 'Username',
  label: 'Username'
)

It's because this code does not belong to a template. Hanami provides us with parts, as a way to decorate our exposures, and all the data we need to set this up we have on the form exposure, except the form builder.

I could replace this all with

<% form.username_scope(f).
  render("shared/forms/text_field")
%>

Or instead of returning the scope from the part I can even hide the rendering inside the part's method. I will go with the latter approach, though I a not sure which one is better yet, as those concepts are just too new. Looking for your feedback on this!

<% form.username_input(f) %>

Now let me create the missing methods in my part.

Step 3. Final sprint - Setting up the Part

In my registration form part I want to have a method for each input field, that accepts the form builder.

Then each method will render the correct field type, with a separate scope prepared. Again, the actual rendering may belong to a template, but I guess it's just a preference. As long as we're consistent, we should be fine.

The field name here will be a username, the scope will be always the same so I'll extract that to a constant named SCOPE_CLASS.

SCOPE_CLASS = Scopes::Shared::Forms::Input
          
def username_input(f)
  prepare_scope(f, :username)
    .render("shared/forms/text_field")
end

private

def prepare_scope(f, field_name, **options)
  scope(
    SCOPE_CLASS,
    f: f,
    field_name: field_name,
    errors: errors(field_name),
    **options
  )
end

Then I can repeat that for the remaining scopes form fields, with minimal changes. I need just a field name to be changed, and the partial name to be rendered, and the rest is just the same, except the tac field.

# frozen_string_literal: true

module Main
  module Views
    module Parts
      module Forms
        class Registration < Part
          SCOPE_CLASS = Scopes::Shared::Forms::Input
          
          def username_input(f)
            prepare_scope(f, :username).
              render("shared/forms/text_field")
          end

          def password_input(f)
            prepare_scope(f, :password).
              render("shared/forms/password_field")
          end

          def password_confirmation_input(f)
            prepare_scope(f, :password_confirmation).
              render("shared/forms/password_field")
          end

          def tac_input(f)
            # TODO: TAC input field
          end

          private

          def prepare_scope(f, field_name, **options)
            scope(
              SCOPE_CLASS,
              f: f,
              field_name: field_name,
              errors: errors(field_name),
              **options
            )
          end

          def errors(key)
            value.dig(:errors, key).to_a
          end
        end
      end
    end
  end
end

Before though, let me clean things up a bit. I'll move the errors to the private section, as this won't be used anymore from the template, then I'm going to setup the correct icon names for the fields.

Now to the tac field rendering. Only this field needs a custom label that I need to prepare, and paste that as an option parameter to the scope.

def tac_input(f)
  tac_link = helpers.link_to "terms and conditions", "#"
  pp_link = helpers.link_to "privacy policy", "#"
  label_text = "I agree to the #{ tac_link } & #{ pp_link }".html_safe
  prepare_scope(f, :tac, label: label_text).
    render("shared/forms/check_box_field")
end

Now that drives us again to the template form where we can replace the complicated HTML with our part methods. This cannot be more clean.

<%= form_for :registration, routes.path(:register_account) do |f| %>
  <%= form.username_input(f) %>
  <%= form.password_input(f) %>
  <%= form.password_confirmation_input(f) %>
  <%= form.tac_input(f) %>

  <div class="field is-grouped">
    <div class="control">
      <%= f.submit "Register account", class: "button is-link" %>
    </div>
    <div class="control">
      <%= link_to "Already have an account", "#", class: "button is-link is-light"%>
    </div>
  </div>
<% end %>

You'll miss the password_field partial yet, but that's identical to the input_field, except the method used on the form builder, so I can prepare that quickly before running the app.

Let me check now what happens in the browser, as I've written quite a bunch of code at once. There is an error saying, that icon_name is called, but it is not passed into the scope in one call. Let me check. Oh, it's because our checkbox has no icon passed, but I still call this method on the conditional. I'll just make it optional. Can be solved in the part, but that's fine for now.

Now the form works flawlessly, as before, but our template is as slim as one can imagine.

Summary

Working with templates is a hard job and eliminating the logic out of them is absolutely not trivial. Hanami provides us with a bunch of tools that help us maintain this complexity and keep our templates always simple and clean. But there are still multiple tradeoffs.

You always need to balance between how simple and skinny your template is, and how easy it is to update by frontend developers, which means, how much ruby code leaks to it.

I hope that this and previous episodes about the topic show you different approaches and that you'll find them useful! If you want to see more content in this fashion, Subscribe to my YT channel, Newsletter and follow me on Twitter!

Thanks

I want to especially thank my recent sponsors,

and all the Hanami Mastery PRO subscirbers, for supporting this project, I really appreciate it!

Consider sponsoring?

If you want to support us, check out our Github sponsors page or join Hanami Mastery PRO as mentioned just before!

Thank you and have a happy rest of your day!

Do you know great Ruby gems?

Add your suggestion to our discussion panel!

I'll gladly cover them in the future episodes! Thank you!

Suggest topicTweet #suggestion

May also interest you...

#52 Render flash messages in Hanami, but like a PRO
hanamiviews

Showing flash messages in Hanami is trivial, and it is even shown in the official guides. In this episode though, we make this future-proof, testable, and maintainable for a future growth of your application.

Probably any web app nowadays requires font icons to be loaded this or other way. In this episode, I'm showing the integration of Font Awesome icons in Hanami 2 applications.

There is a famous video about the IT skill iceberg - but thankfully, there is an alternative. If you ever wondered if you can build entire websites without HTML overhead, now you can! Meet Phlex, a view engine, where you can write Ruby instead of HTML

With the release of Hanami 2.1, the view layer is fully integrated with our Hanami applications. Let's check how to make use of a form helper to send a contact email.

Coffee buy button
Trusted & Supported by
AscendaLoyalty

1 / 1
Open Hanami Jobs

We use cookies to improve your experience on our site. To find out more, read our privacy policy.