Abstracting common HTML fragments

I’m creating a new Hanami application using Bootstrap. When dealing with forms, I have a lot of fragments like this:

form_for :link, routes.link_path do
  div class: 'container' do
    div class: 'form-group row' do
      label :url, class: 'col-sm-2 col-form-label'
      div class: 'col-sm-10' do
        text_field :url, class: 'form-control'
      end
    end

    div class: 'form-group row' do
      label :title, class: 'col-sm-2 col-form-label'
      div class: 'col-sm-10' do
        text_field :title, class: 'form-control'
      end
    end

    div class: 'form-group row' do
      label :lead, class: 'col-sm-2 col-form-label'
      div class: 'col-sm-10' do
        text_area :lead, class: 'form-control'
      end
    end
  end
end

Since it’s a lot of unnecessary repetition, I would like to abstract out part of it and have, say:

form_for :link, routes.link_path do
  div class: 'container' do
    item(:text_field, :url)
    item(:text_field, :title)
    item(:text_area, :lead)
  end
end

Tried with:

module Web::Views
  module Forms
    include Hanami::Helpers

    def item(type, name)
      div class: 'form-group row' do
        label name, class: 'col-sm-2 col-form-label'
        div class: 'col-sm-10' do
          text_field name
          public_send(type, name, class: 'form-control')
        end
      end
    end
  end
end

… but it does not work. Any ideas how to approach this? Is that possible to achieve something like that?

PS. By “not work” I mean:

ArgumentError: wrong number of arguments (given 2, expected 1)

/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering.rb:198:in `method_missing'
/Users/pawel/dev/priv/baiter/apps/web/views/forms.rb:6:in `item'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering/scope.rb:69:in `method_missing'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:383:in `method_missing'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:383:in `method_missing'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:5:in `block (3 levels) in singleton class'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:364:in `instance_exec'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:364:in `resolve'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:58:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/html_node.rb:49:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:44:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `map'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/form_builder.rb:140:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:61:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/html_node.rb:49:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:44:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `map'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/form_builder.rb:140:in `to_s'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:35:in `block in singleton class'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:-6:in `instance_eval'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:-6:in `singleton class'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:-8:in `__tilt_70145548759920'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/tilt-2.0.7/lib/tilt/template.rb:170:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/tilt-2.0.7/lib/tilt/template.rb:170:in `evaluate'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/tilt-2.0.7/lib/tilt/template.rb:109:in `render'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/template.rb:41:in `render'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering.rb:139:in `rendered'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering.rb:153:in `layout'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering.rb:107:in `render'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering.rb:258:in `render'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-1.0.0/lib/hanami/rendering_policy.rb:56:in `_render_action'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-1.0.0/lib/hanami/rendering_policy.rb:48:in `_render'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-1.0.0/lib/hanami/rendering_policy.rb:38:in `render'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-1.0.0/lib/hanami/application.rb:169:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/2.3.0/delegate.rb:83:in `method_missing'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/http_router-0.11.2/lib/http_router.rb:193:in `process_destination_path'
(eval):15:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/http_router-0.11.2/lib/http_router.rb:288:in `raw_call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-router-1.0.0/lib/hanami/routing/http_router.rb:156:in `raw_call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/http_router-0.11.2/lib/http_router.rb:142:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-router-1.0.0/lib/hanami/router.rb:1016:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/method_override.rb:22:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-1.0.0/lib/hanami/assets/static.rb:49:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/content_length.rb:15:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/common_logger.rb:33:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/builder.rb:153:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-1.0.0/lib/hanami/app.rb:42:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/shotgun-0.9.2/lib/shotgun/loader.rb:86:in `proceed_as_child'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/shotgun-0.9.2/lib/shotgun/loader.rb:31:in `call!'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/shotgun-0.9.2/lib/shotgun/loader.rb:18:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-1.0.0/lib/hanami/assets/static.rb:49:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/lint.rb:49:in `_call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/lint.rb:37:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/show_exceptions.rb:23:in `call'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/handler/webrick.rb:86:in `service'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/2.3.0/webrick/httpserver.rb:140:in `service'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/2.3.0/webrick/httpserver.rb:96:in `run'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/2.3.0/webrick/server.rb:296:in `block in start_thread'

Hi,
To be sure, is this line that raises the error?

div class: 'form-group row' do

That should be html.div, because you’re outside of the form_for context, so the HTML builder is accessible via the html method.

Sorry for crappy report :wink: Yes, it’s about this line. I also tried with adding html, but nothing is rendered then. Is it possible that it’s different instance of HTML builder?

Does not work even in simplest case:

def item(type, name)
  html.p 'test'
end

@katafrakt The HTML builder internally stores a nested structure that reflects the calls to the tag helpers. For this reason you’re able to write the following code without explicit concatenation:

div do
end

p do
end

If you look, there is no concatenation between the two helpers, because their calls are stored internally. So when the HTML is rendered this structure is serialized into tags.


Getting back to your example, item isn’t known by the HTML builder, so you have to force the concatenation:

    div class: 'container' do
      span +
        item(:text_field, :url) +
        item(:text_field, :title) +
        item(:text_area, :lead)
    end

This works for me.


The actual implementation I used to try that code above is:

    def item(type, name)
      html.div class: 'form-group row' do
        label name, class: 'col-sm-2 col-form-label'

        div class: 'col-sm-10' do
        end
      end
    end

It doesn’t work when both text_field or public_send are used. Can you please try to understand why? Thanks :wink:

Because we are in HtmlBuilder and not FormBuilder context, so text_field is not defined and the call hits method_missing of HtmlBuilder, right?

Anyway, using concatenation is not really what I wanted, but following your explanation I achieved what I wanted by opening FormBuilder class like that:

module Hanami::Helpers::FormHelper
  class FormBuilder
    def bs_form_item(type, name)
      div class: 'form-group row' do
        label name, class: 'col-sm-2 col-form-label'
        div class: 'col-sm-10' do
          public_send(type, name, class: 'form-control')
        end
      end
    end
  end
end

I renamed item to bs_form_item to better explain what it does. It’s hacky (opening a class) but works. Is there a better solution to it? I assume people are going to want something like that in future.

Not now, but we can think of it.

Is there any way to apply the following solution?

module Bootstrap
  class Form < Hanami::Helpers::FormHelper::FormBuilder
    def item(...)
    end
  end

  module Helpers
    def form_for(*args)
      Form.new(*args)
    end
  end
end

And then in all the views you include that Bootstrap::Helpers.

@jodosha Unfortunately I didn’t manage to make it work so far, but I’ll keep trying :wink:

First of all, the reason is that form_for and FormBuilder#initialize have very different signatures, so just passing arguments around won’t work. I tried different solution by extending original form for, so that different FormBuilder can be injected:

module Hanami::Helpers::FormHelper
  def form_for(name, url, options = {}, &blk)
    form = if name.is_a?(Form)
             options = url
             name
           else
             Form.new(name, url, options.delete(:values))
           end
    form_builder_class = options.fetch(:form_builder, FormBuilder)

    attributes = { action: form.url, method: form.verb, :'accept-charset' => DEFAULT_CHARSET, id: "#{form.name}-form" }.merge(options)
    form_builder_class.new(form, attributes, self, &blk)
  end
end

and then create a builder like you suggested:

module Web::View::Bootstrap
  class Form < Hanami::Helpers::FormHelper::FormBuilder
    def bs_form_item(type, name)
      div class: 'form-group row' do
        label name, class: 'col-sm-2 col-form-label'
        div class: 'col-sm-10' do
          public_send(type, name, class: 'form-control')
        end
      end
    end
  end

  module Helpers
    def form_for(name, url, options = {}, &blk)
      super(name, url, options.merge(form_builder: Form), &blk)
    end
  end
end

But I keep getting this error, even though I checked that correct FormBuilder has been instantiated:

NoMethodError: undefined method `bs_form_item' for #<Hanami::View::Rendering::Scope:0x007fe062380f00>

/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering/layout_scope.rb:221:in `method_missing'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-view-1.0.0/lib/hanami/view/rendering/scope.rb:73:in `method_missing'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:383:in `method_missing'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:383:in `method_missing'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:6:in `block (3 levels) in singleton class'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:364:in `instance_exec'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:364:in `resolve'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:58:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/html_node.rb:49:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:44:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `map'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/form_builder.rb:140:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:61:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/html_node.rb:49:in `content'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_node.rb:44:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `map'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/html_helper/html_builder.rb:332:in `to_s'
/Users/pawel/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/hanami-helpers-1.0.0/lib/hanami/helpers/form_helper/form_builder.rb:140:in `to_s'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:36:in `block in singleton class'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:-6:in `instance_eval'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:-6:in `singleton class'
/Users/pawel/dev/priv/baiter/apps/web/templates/link/new.html.erb:-8:in `__tilt_70300865545520'

Hey there, new Hanami user here! I’m really interested in this issue as I hit the same roadblocks @katafrakt except trying to make a form builder for Semantic UI instead of Bootstrap. Isn’t injecting a custom FormBuilder into form_for something good and cheap to have, like Rails has? I’m curious to know if there was a particular design decision that left this feature out, I suspect there was because I really like the general design of all the Hanami gems I’ve seen.

Thanks

For bootstrap helpers, we use refinements to change enhance the FormBuilder methods. This way we use the same Hanami form API and can choose when we want the refinements. It’s been great so far. Ping me if you want a code example

I’d like to see an example of this.

# frozen_string_literal: true

# Overrides to make bootstrap integrate better with forms
module Franchises
  module ViewHelpers
    module Bootstrap
      module Form
        refine Hanami::Helpers::FormHelper::FormBuilder do
          %w[
            check_box
            color_field
            date_field
            datetime_field
            datetime_local_field
            email_field
            file_field
            number_field
            password_field
            radio_button
            select
            text_area
            text_field
          ]. each do |field_name|
            define_method field_name do |*args, include_label: true, label_text: nil, **kwargs|
              div(class: 'form-group') do
                label(label_text || args[0]) if include_label
                super(*args, **kwargs, class: 'form-control')
                div(class: 'invalid-tooltip')
                div(class: 'invalid-tooltip--pointer')
              end
            end

            define_method :submit do |*args, **kwargs|
              kwargs[:class] = 'btn btn-primary' unless kwargs.key?(:class)
              super(*args, **kwargs)
            end
          end
        end
      end
    end
  end
end
# in a view somewhere
module Admin
  module Views
    module GeolocationPages
      class Edit
        include Admin::View
        using ::Franchises::ViewHelpers::Bootstrap::Form

        def form
          form_for(
            :geolocation_page,
            routes.geolocation_page_path(geolocation_page.id),
            method: :patch,
            class: 'geolocation-page__edit-form',
            values: { geolocation_page: geolocation_page }
          ) do
            text_field :title
            text_field :subtitle
            text_field :location_name
            text_field :postal_code
            text_field :slug
            check_box :published
            submit 'Submit'
          end
        end
      end
    end
  end
end