Hanami 2.1.1 form_for helper not generating expected markup

Ruby: 3.3.3
Hanami: 2.1.1

Following is a method I have in my view scope

def update_form
  form_for('update-form', '/update_data', method: :patch) do |f|
    tag.div(class: 'form-control') do
      tag.label('Update Form')
    end

    tag.div(class: 'form-control') do
      tag.label("Select Age")

      tag.select(name: :age, class: 'age') do
        tag.option( 'Select an Age', value: empty_string )

        [ 20, 25, 30, 35 ].each do |age|
          tag.option( age, value: age )
        end
      end
    end

    tag.div(class: "form-control") do
      f.button("Update", type: 'button', class: "button-primary update-btn")
      f.button('Cancel', type: 'button', class: "button-primary cancel-update-btn")
    end
  end
end

In view-template app/templates/..../update_data.html.haml I have following code:

%div.update-form-container<
  = update_form

When I access the page in browser and inspect the DOM it contains following markup:

<div class="update-form-container">
    <form action="/update_data" method="POST" accept-charset="utf-8">
        <input type="hidden" name="_method" value="PATCH">
        <input type="hidden" name="_csrf_token" value="195da3c1adfeeaf482800a3f29b1ddcc65185f08a98e3a7971ef59bdc2916ced">
        <div class="form-control">
            <button type="button" class="button-primary cancel-update-btn">Cancel</button>
        </div>
    </form>
</div>

Experimenting with the code in form_for block found that only the last evaluated markup is considered as the content.

For e.g. if I modify update_form method as following

def update_form
  form_for('update-form', '/update_data', method: :patch) do |f|
    tag.div(class: 'form-control') do
      tag.label('Update Form')
    end

    tag.div(class: 'form-control') do
      tag.label("Select Age")

      tag.select(name: :age, class: 'age') do
        tag.option( 'Select an Age', value: empty_string )

        [ 20, 25, 30, 35 ].each do |age|
          tag.option( age, value: age )
        end
      end
    end
  end
end

and then refreshing the page when I inspected the DOM it contains following markup:

<div class="update-form-container">
    <form action="/update_data" method="POST" accept-charset="utf-8">
        <input type="hidden" name="_method" value="PATCH">
        <input type="hidden" name="_csrf_token" value="195da3c1adfeeaf482800a3f29b1ddcc65185f08a98e3a7971ef59bdc2916ced">
        <div class="form-control">
            <select name="age" class="age">[20, 25, 30, 35]</select>
        </div>
    </form>
</div>

Can anybody please guide me on the correct usage of form_for so as to generate following desired markup

<div class="update-form-container">
    <form action="/update_data" method="POST" accept-charset="utf-8">
        <input type="hidden" name="_method" value="PATCH">
        <input type="hidden" name="_csrf_token" value="195da3c1adfeeaf482800a3f29b1ddcc65185f08a98e3a7971ef59bdc2916ced">

        <div class="form-control">
            <label>Update Form</label>
        </div>

        <div class="form-control">
            <label>Select Age</label>

            <select name='age', class: 'age'>
              <option value=''>Select an Age</option>
              <option value='20'>20</option>
              <option value='25'>25</option>
              <option value='30'>30</option>
              <option value='35'>35</option>
            </select>
        </div>

        <div class="form-control">
            <button type="button" class="button-primary update-btn">Update</button>
            <button type="button" class="button-primary cancel-update-btn">Cancel</button>
        </div>
    </form>
</div>

Unfortunately, the intermediate results within blocks aren’t captured like that. You may be able to concatenate them with + but I had mixed results with that (sometimes it resulted in outputting the HTML source). It might be nice to add something like a concat method to build the response within the block (or do it automagically somehow)

For now, I’d just put this in the template, then experiment with extracting a Part class.

As an aside, there’s a a draft guide for the form helpers, not sure if you saw that yet. So you could use the f form builder object that builds the options for you.

f.select(
  :age, 
  [20, 25, 30, 35 ], 
  options: { prompt: 'Select an Age' }, 
  class: 'age'
)

Though I see this disables the prompt, which is incorrect and I’ll fix in a PR soon.

Thanks @cllns for your reply.

Unfortunately, the intermediate results within blocks aren’t captured like that. You may be able to concatenate them with + but I had mixed results with that (sometimes it resulted in outputting the HTML source). It might be nice to add something like a concat method to build the response within the block (or do it automagically somehow)

Exploring the implementation of form_for found the culprit to be the yield used in following line

which should always return the result of the last evaluated statement.

Regarding following

Actually the update_form method I have shown in my foremost post is an updated version of following original version

def update_form
  form_for('update-form', '/update_data', method: :patch) do |f|
    div(class: 'form-control') do
      label('Update Form')
    end

    div(class: 'form-control') do
      label("Select Age")

      tag(:select, name: :age, class: 'age') do
        option( 'Select an Age', { value: '' } )

        [ 20, 25, 30, 35 ].each do |age|
          option( age, { value: age } )
        end
      end
    end

    div(class: "form-control") do
      button("Update", type: 'button', class: "button-primary update-btn")
      button('Cancel', type: 'button', class: "button-primary cancel-update-btn")
    end
  end
end

which used form_for helper which was part of hanami/helpers gem. And that helper worked in expected manner while using it with hanami/hanami gem v2.0.3.

But after I started upgrading hanami to v 2.1.1 I ended up updating my method update_form to the version shown in my foremost post. And thus my working code started breaking.

As per Port helpers from hanami-helpers, introduce TagHelper by timriley · Pull Request #229 · hanami/view · GitHub the plan was to port the helpers but while porting the behaviour also got changed as is evident from what I have reported in this post, so that way it is disappointing for me because after upgrading hanami 2.1.1 I am now struggling to restore my working methods returning HTML form markup and I have a lot of them and all of them are defined in view scope.

And I took the approach of defining my markup generation methods in scope for following reason:

While exploring hanami-view gem docs I came across following in docsite/source/scopes.html.md on hanami/view gem v2.1.0 codebase on github

With a custom scope, you can add your own behavior around a template and its particular set of locals. These, along with parts, allow for most view logic to move away from templates and into classes you can reuse, refactor according to typical object oriented approaches, as well as test in isolation.

Keeping in my mind that purpose and advantage of using Scopes and also that hanami/helpers gem provided helpers for markup generation and which can be used inside Ruby classes I decided to keep, as much as possible, all my templates html-tags free so as to make them look clean and simple-looking.

And that turned out a very wise decision and also it helped a lot in reusing a lot of markup-related logic.

But after I started the hanami upgrade I am now entangled in fixing the breaking API and the most intensive of it in my case is this form helpers API.

Regarding following

As an aside, there’s a [a draft guide for the form helpers], not sure if you saw that yet. So you could use the f form builder object that builds the options for you.

I did found that guide while I was looking at Issue 1424 in Issues list for hanami/hanami gem on github. And in my update_form method I shown I deliberately used

tag.option( age, value: age )

to manually generate the option tags because the convenience the form_builder.select provides for generating options doesn’t have the provision to attach custom data (like data-attrs) to each option tag (for e.g. below is an example markup attaching custom data to option tags)

  <select name='age', class: 'age'>
    <option value=''>Select an Age</option>
    <option value='20' data-attr-db-id='56'>20</option>
    <option value='25' data-attr-db-id='57'>25</option>
    <option value='30' data-attr-db-id='58'>30</option>
    <option value='35' data-attr-db-id='59'>35</option>
  </select>

You can refer the L991-L1013 in the select method implementation in lib/hanami/helpers/form_helper/form_builder.rb in hanami/hanami gem v2.1.1 code at to validate my finding.

Thanks.

Thanks for all that info! Indeed, that does look like the problem.

Do you think you can open a PR in hanami/hanami with a failing spec (or a few) that illustrates this problem? @timriley will know better since he’s the one who did all that work, but I think this is something we want to support

Hi @jignesh, thanks for your very detailed reports, and sorry for my delay in replying, I’ve been away from home this week.

Unfortunately, it looks like you got caught across an awkward transition: while the 1.x (or subsequent unreleased) versions of hanami-helpers may have worked with Hanami 2.0, we didn’t officially support this. Then as you noticed via the afore-linked PR, when we did reintroduce helpers with 2.1, we adjusted the form/tag helpers so they could be used most naturally within templates, which was not possible how they worked in hanami-helpers 1.x.

I’m very sorry for the inconvenience this has caused you.

Here are some workarounds that may help you:

  1. Move the forms into partials. You can still render these partials from within view parts and scopes.
  2. Or if you want to keep the helpers where they are, then inside the blocks you’re giving these, concat any inner tags onto a single string that you then return at the end of each block. (I acknowledge that this would likely lead to ungainly code).

I’d certainly be interested in making these helpers more useful when they’re being used in “pure ruby mode”, i.e. not within templates. If you have ideas on how this could look, I’d love to hear them! The one constraint I’d place on this is that any changes in this direction shouldn’t compromise or confuse the experience of using these helpers within templates.

@cllns Sorry, it is not possible for me to do that presently.

@timriley Thanks for your reply and your suggested workarounds. They definitely sound useful but in my case I can employ none of them for following reasons:

For 1st workaround I would need to introduced a lot of partials which is possible but I don’t find it practical in my case.

The 2nd one of concatenating inner tags would make my code very much untidy.

So I will need to think of implementing a helper which can wrap my current code and produces the desired concatenated output.

Thank you to both of you for sparing time to share your inputs. Really appreciate it.