Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow f.select to be called with a single hash containing options and HTML options #46629

Merged
merged 1 commit into from Dec 13, 2022

Conversation

ghiculescu
Copy link
Member

@ghiculescu ghiculescu commented Dec 1, 2022

Motivation / Background

I do this a lot:

<%= select :post, :author, authors, required: true %>

It doesn't work; the required attribute is ignored! Instead, you need to do this:

<%= select :post, :author, authors, {}, required: true %>

It's hard to remember the right API, and it looks to me like a code smell. It looks even smellier when you end up with this:

<%= select :post, :author, authors, { include_blank: "Choose an option" }, { required: true } %>

Where this would be nicer, but again, the required attribute is ignored:

<%= select :post, :author, authors, include_blank: "Choose an option", required: true %>

Detail

This PR implements a special handling for required, multiple, and size HTML attributes so that these now do the same thing:

<%= select :post, :author, authors, include_blank: "Choose an option", required: true %>
<%= select :post, :author, authors, { include_blank: "Choose an option" }, { required: true } %>

Additional information

As proof I'm not the only person who makes this mistake, one of the tests in the Rails test suite was wrong! The test added in #40522 puts the multiple attribute in the wrong place and has the wrong assertion as as result. This PR includes a fix for the test.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.
  • CI is passing.

@skipkayhil
Copy link
Member

<%= select :post, :author, authors, {}, required: true %>

I wrote something like this just today after going through the same steps of forgetting that it needed a second options hash to work, so I appreciate you looking to solve this!

In my case, I was adding a class to a select so I don't think this change would help that. I wonder if there's a way we could take more advantage of the separation of positional and keywords args to make options more optional 🤔

@ghiculescu
Copy link
Member Author

Hmm 🤔 if you maintained an allowlist of attributes for options, then you could push any other given option to html_options, if no HTML options are provided.

I think the allowlist would be %i[include_blank prompt disabled include_hidden selected], can you think of any others?

@skipkayhil
Copy link
Member

I think the allowlist would be %i[include_blank prompt disabled include_hidden selected], can you think of any others?

I had a similar thought, and was actually surprised that the docs don't list the available options: https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-select

I was also curious if a similar pattern could be applied to other form helpers with html_options, however the list of options is not always so short: https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-date_select

Maybe this is a case where the change makes sense for select even if it isn't consistent with other tags?

@ghiculescu
Copy link
Member Author

I wonder why distinction between options and html_options even exists. None of the other form helpers have html_options, just select tags.

@casperisfine
Copy link
Contributor

Isn't there a backward compatibility issue here? What if an app has size or required as a valid <option value>?

@ghiculescu
Copy link
Member Author

Those attributes don't get passed down to individual option tags, so I don't think so (but it's possible I've misunderstood what you mean...).

@casperisfine
Copy link
Contributor

Nah, I'm the one who was confused.

@ghiculescu
Copy link
Member Author

@casperisfine did you have any opinions on the change and/or on the allowlist ideal discussed above?

@casperisfine
Copy link
Contributor

Not really, but I don't do much frontend these days.

But since no-one else reviewed it, I'll do it tomorrow. At first sight I don't see any issues with it, I'm just always a bit put of by "brittle" argument processing. But in this case I see how it's much more ergonomic so...

Copy link
Member

@byroot byroot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, please rebase and I'll merge.

…nd HTML options

I do this a lot:

```erb
<%= select :post, :author, authors, required: true %>
```

It doesn't work; the `required` attribute is ignored! Instead, you need to do this:

```erb
<%= select :post, :author, authors, {}, required: true %>
```

It's hard to remember the right API, and it looks to me like a code smell. It looks even smellier when you end up with this:

```erb
<%= select :post, :author, authors, { include_blank: "Choose an option" }, { required: true } %>
```

Where this would be nicer, but again, the `required` attribute is ignored:

```erb
<%= select :post, :author, authors, include_blank: "Choose an option", required: true %>
```

This PR implements a special handling for `required`, `multiple`, and `size` HTML attributes so that these now do the same thing:

```erb
<%= select :post, :author, authors, include_blank: "Choose an option", required: true %>
<%= select :post, :author, authors, { include_blank: "Choose an option" }, { required: true } %>
```

ps. as proof I'm not the only person who makes this mistake, one of the tests in the Rails test suite was wrong! The test added in rails#40522 puts the `multiple` attribute in the wrong place and has the wrong assertion as as result. This PR includes a fix for the test.
@byroot byroot merged commit f4f954f into rails:main Dec 13, 2022

*Alex Ghiculescu*

* Datetime form helpers (`time_field`, `date_field`, `datetime_field`, `week_field`, `month_field`) now accept an instance of Time/Date/DateTime as `:value` option.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @kryzhovnik I fixed a typo here (Datatime -> Datetime).

@ghiculescu ghiculescu deleted the select-merge-options branch December 13, 2022 17:51
@ghiculescu
Copy link
Member Author

Implemented this in a small app. So much better already. Thanks @byroot !

@morgoth
Copy link
Member

morgoth commented Dec 13, 2022

@ghiculescu If one wants to set some class on the select element, does it still require to write <%= select :post, :author, authors, {}, class: "some-css-class" %>?

@ghiculescu
Copy link
Member Author

Right now, yes. What did you think of this idea?

@morgoth
Copy link
Member

morgoth commented Dec 13, 2022

I think that would be much better.
With this change merged, it will be even more confusing as one will not be sure which options are allowed in one hash and which ones still require additional {} before.

@ghiculescu
Copy link
Member Author

ghiculescu commented Dec 14, 2022

Started working on this. Just a note on something I've observed so far which might explain why options and html_options are separate hashes.

Both accept a :disabled key.

  • In the options hash, disabled is passed to options_for_select and is used to determine which <option>s should have the disabled attribute. It expects an array of strings.
  • In the html_options hash, disabled expects a boolean. If true, then the entire <select> gets the disabled attribute.

This means that it would not be possible to have an API where you never need to think about the additional hash.

Here's some tests for the two options, both pass on main:

  def test_select_with_disabled_html_options_and_disabled_selected_option
    @post = Post.new
    @post.category = "foo"
    assert_dom_equal(
      "<select class=\"disabled\" disabled=\"disabled\" name=\"post[category]\" id=\"post_category\"><option value=\"\" label=\" \"></option>\n<option selected=\"selected\" value=\"foo\">foo</option>\n<option value=\"bar\">bar</option></select>",
      select("post", "category", %w(foo bar), { prompt: true, include_blank: true }, { class: "disabled", disabled: true })
    )
  end

  def test_select_with_disabled_option
    @post = Post.new
    @post.category = "foo"
    assert_dom_equal(
      "<select name=\"post[category]\" id=\"post_category\"><option value=\"\" label=\" \"></option>\n<option selected=\"selected\" value=\"foo\">foo</option>\n<option disabled=\"disabled\" value=\"bar\">bar</option></select>",
      select("post", "category", %w(foo bar), { prompt: true, include_blank: true, disabled: ["bar"] })
    )
  end

@casperisfine
Copy link
Contributor

Both accept a :disabled key.

Urk, this makes me want to revert to be honest, as this is extremely confusing. What do you think @ghiculescu ?

@ghiculescu
Copy link
Member Author

I still think this PR is fine but not sure if we should go further. My main evidence (apart from me liking it) is that some of the tests in Rails made the same mistake.

Of course that’s all fairly subjective. I won’t be upset if you revert, but we should fix the broken test if you do (I can do that).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants