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

Handle nil form_with model argument #49943

Merged
merged 1 commit into from
Jan 4, 2024

Conversation

cjilbert504
Copy link
Contributor

@cjilbert504 cjilbert504 commented Nov 7, 2023

Motivation / Background

First, thank you to everyone here for taking the time to read this description.

I would like to propose a possible change to the form_with method which would help provide, in my opinion, better debugging around an issue that can be encountered when passing a nil object to the model: argument in the form_with method.

I have come across this issue a few times myself and I know others that have as well. The most recent encounter was from a newer Rails developer that myself and others have been helping along in the process of learning Rails.

The issue actually shows up in two ways, the first is if the form is on an index page, having nil passed to the :model argument this will raise an ActionController::ParameterMissing error. The other is in the default scenario of being on the new page and having nil passed to the :model argument. In this scenario, an ActionController::RoutingError (No route matches [POST] "/posts/new") (using Post model as an example). This happens because of Rails falling back to building the URL from the current page in this scenario.

Going back to the first form of this causing an error, I would like to outline the flow of the issue so that everyone can (hopefully) have a better understanding of this issue.


Junior Dev: I'm having an issue using strong parameters. Has there been a change in rails 7 when using strong parameters?

def url_link_params
  params.require(:url_link).permit(:url)
end

where the model name is UrlLink. When I try to submit my form, I get the following error:

ActionController::ParameterMissing (param is missing or the value is empty: url_link)

When I remove require and just use params.permit(:url), I can submit the form.
I'm confused, I setup my form with form_with(model: url_link) and have a url input field form.url_field :url.

Me: I see that you have a mismatch in instance variable naming between the instance variable declared in the controller action versus what is being passed in as a local to the form partial:

def index
  @link = UrlLink.new
end

<%= render partial: "form", locals: { url_link: @url_link } %>

Junior Dev: Thanks, I fixed the mismatch and my form and controller are working now.

With this context, I think it would have been a better debugging experience for the junior if instead of the ActionController::ParameterMissing error being raised thus causing the junior to think it was a strong params issue (which it sort of is I admit) I think it would be more helpful to raise an ArgumentError when passing in a nil object to the model: argument in form_with.


Detail

This PR changes the default value of the model: argument in form_with to false instead of nil and raises an ArgumentError in the event that the argument is nil.

def form_with(model: false, other_args)
  raise ArgumentError, "Form model object is nil" if model.nil?
end

I tried to think reasons/scenarios of why/where false wouldn't be a suitable replacement for nil as the default value for the model: argument but I couldn't think of any (I'm not saying there isn't a case though and would love to hear so if there are any).

It also changes a local variable that is set in a case statement in the form_for method from nil to false which is needed to allow form_for to continue to call form_with successfully.

Additional information

Thank you for taking the time to read this and for any thoughts or feedback about it.

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.

@rails-bot rails-bot bot added the actionview label Nov 7, 2023
@cjilbert504 cjilbert504 changed the title Handle nil form model object Handle nil form model argument Nov 7, 2023
@cjilbert504 cjilbert504 changed the title Handle nil form model argument Handle nil form_with model argument Nov 7, 2023
@excid3
Copy link
Contributor

excid3 commented Nov 7, 2023

@rafaelfranca when we were talking about issues that beginners face at Rails World, this is one of the most common we've seen. Instance variables returning nil instead of an undefined variable error makes it confusing to beginners who aren't the most careful with variable naming yet.

@mattpolito
Copy link
Contributor

I was under the impression that when form_with was released, it was to eventually be a single interface replacement for form_for and form_tag. This would mean that it's supposed to work without a model. You could just give it a url and get a form helper object to work with.

I think the PR is great but the functionality would only apply to form_for.

@mattpolito
Copy link
Contributor

@excid3 I completely agree with the instance variable headaches for beginners. It's one of the reasons we developed decent_exposure. It encapsulates what they're trying to do with an instance variable into a view helper. Since it gets turned into an actual method, Ruby gives way more help with things like a typo.

@cjilbert504
Copy link
Contributor Author

cjilbert504 commented Dec 6, 2023

@mattpolito - Hey there!

This would mean that it's supposed to work without a model. You could just give it a url and get a form helper object to work with.

This change still allows that functionality. This is handling when you make a call something like form_with(model: @post) but @post is nil. I don't know when you would ever intentionally pass nil to the :model argument, though I'd love to hear a reason from someone.

I think the PR is great but the functionality would only apply to form_for.

Thanks! I'm glad you like the PR. I don't know that I agree with you on moving it to form_for though, especially if as you thought, form_with was going to be a replacement plus you can freely use both methods and form_for just calls form_with eventually.

@mattpolito
Copy link
Contributor

@cjilbert504 I just mean not passing model to form_with is a valid use-case. However, after reviewing the code again I see the distinction you made between the false and the nil check.

@cjilbert504
Copy link
Contributor Author

@mattpolito - It definitely is a valid use-case which is why I didn't want to break that existing functionality. Thanks for chiming in on this one!

actionview/CHANGELOG.md Outdated Show resolved Hide resolved
@p8
Copy link
Member

p8 commented Dec 8, 2023

The :model argument is supposed to be a model object, so this change makes sense to me:

# * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and
# <tt>:scope</tt> by, plus fill out input field values.

This also makes form_with inline with form_for, which also raises if the model is nil.

raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object

@p8 p8 added the ready PRs ready to merge label Dec 10, 2023
@rafaelfranca rafaelfranca merged commit 4544b0d into rails:main Jan 4, 2024
4 checks passed
@cjilbert504 cjilbert504 deleted the handle-nil-form-model-object branch January 4, 2024 13:47
henrikbjorn added a commit to henrikbjorn/passwordless that referenced this pull request Jan 9, 2024
rails/rails#49943

`form_with` cannot have `model: nil` anymore. Make sure to set `@session` if no `authenticable` can be found.
mikker pushed a commit to mikker/passwordless that referenced this pull request Jan 9, 2024
rails/rails#49943

`form_with` cannot have `model: nil` anymore. Make sure to set `@session` if no `authenticable` can be found.
@jhawthorn
Copy link
Member

This PR changes the default value of the model: argument in form_with to false instead of nil and raises an ArgumentError in the event that the argument is nil.

This sounds like a breaking change, which we try to guarantee to users that they will see one minor version with a deprecation notice. Could we change this ArgumentError to a deprecation warning for Rails 7.2 and only raise the argument error in the following version?

@cjilbert504
Copy link
Contributor Author

@jhawthorn - If that's what you all would like to happen here, I'm happy to do so. If that's the case, I will try to get to it ASAP. Thanks for the feedback/thoughts here!

@rafaelfranca
Copy link
Member

Yes. Let's deprecate it.

@cjilbert504
Copy link
Contributor Author

cjilbert504 commented Jan 12, 2024

Copy that! I think I'll have some time on Monday or Tuesday that I can take care of it if that's cool.

@ghiculescu
Copy link
Member

FYI @cjilbert504 an example of where this would break would be if you have a custom form helper that calls form_with internally, it will start raising as of this PR:

  # Renders a Rails form, enhanced with Turbo attributes. Specifically:
  # - Connects the form to the Form stimulus controller.
  # - Calls Form#submitStart when the form is submitted.
  # - Targets the "_top" turbo frame (ie. the full page) if no frame is provided.
  #
  # Accepts the same arguments as +form_with+: https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with
  # You can provide Stimulus options using the `stimulus` keyword. See the +stimulus+ method for options.
  def turbo_form_with(model: nil, scope: nil, url: nil, format: nil, stimulus: {}, turbo_frame: nil, **options, &block)
     form_with(model: model, scope: scope, url: url, format: format, builder: TurboFormBuilder, **form_options, &block)
  end

The fix is easy, but we should warn before we raise.

diff --git a/app/helpers/turbo_view_helper.rb b/app/helpers/turbo_view_helper.rb
index 123b64f6458..c413875ad29 100644
   # Accepts the same arguments as +form_with+: https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with
   # You can provide Stimulus options using the `stimulus` keyword. See the +stimulus+ method for options.
-  def turbo_form_with(model: nil, scope: nil, url: nil, format: nil, stimulus: {}, turbo_frame: nil, **options, &block)
+  def turbo_form_with(model: false, scope: nil, url: nil, format: nil, stimulus: {}, turbo_frame: nil, **options, &block)
      form_with(model: model, scope: scope, url: url, format: format, builder: TurboFormBuilder, **form_options, &block)
    end

@cjilbert504
Copy link
Contributor Author

@ghiculescu - Yep, I understand that it would break as of this PR. I'm going to work on deprecating first as soon as I can. Thanks a bunch!

@cjilbert504
Copy link
Contributor Author

cjilbert504 commented Jan 30, 2024

@jhawthorn @rafaelfranca - Sorry I'm just getting back to wrapping this up (was sick and had power outages due to bad weather). Anyway, I wanted to ask about wrapping this up, would you all like me to make a new branch/PR to undo this and swap to the deprecation warning or just use this same branch and undo and swap to the deprecation warning?

@rafaelfranca
Copy link
Member

It would need to be a new branch/PR.

@cjilbert504
Copy link
Contributor Author

Perfect, because that’s what I did already 🤣. But wanted to confirm first. Thanks, @rafaelfranca

@cjilbert504
Copy link
Contributor Author

@p8 @jhawthorn @rafaelfranca - I opened #50931 to resolve the issue of this PR introducing breaking changes. Let me know if I need to change anything about it and I will happily do so. Thank you all very much!

carlosantoniodasilva added a commit to heartcombo/simple_form that referenced this pull request Apr 9, 2024
Rails main changed `object` to be `false` instead of `nil`, causing the
specific check we were doing for `object.nil?` to fail, and the custom
errors were not displaying for forms without an object. Our test suite
caught that, and the fix is to simply not check specifically for `nil?`,
but just whether we have an object or not, which should account for both
`nil`/`false` versions.

It's a small backwards incompatibility, probably not something worth
changing the framework for, but one worth mentioning.

Source is likely to be:
- rails/rails#49943
- rails/rails#50931
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
actionview ready PRs ready to merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants