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
Why another form object? #3
Comments
I know reform and I did some pull request. I made this gem because I tried reform and I found some bugs. I started to contribute but there is some things I don't like in reform. First, I don't like than the What you think? |
@GCorbel, when I was looking at reform and deciding whether to use it, I didn't like how the I find reform's implementation a bit too complicated too (lots of layers of abstraction, including going through the |
@GCorbel, I am not sure @TylerRick, the reason I need to use `#validate' is because the primary reason I am using a form object is to centralise all logic around parsing params and validating those params in the form object. So the validations are essential from my perspective. |
@TylerRick I already seen the @mattheworiordan Another problem I found with Reform is the synchronisation with models. The object you passed in argument to reform does not have the same value than the form. If you compare the code of Reform and the code of ActiveForm-Rails, I think the last is more simple and clear for a behavior similar (or better). |
@mattheworiordan, I think you're right. But you're right, I think the idea is that you don't want to "pollute" your model objects with that data unless trailblazer/reform#43 (comment)
@apotonick, I think the "save" semantics may be confusing to some people (including me). How about we rename it to @mattheworiordan, I agree that the validations are essential most of the time. By the way, have you found a good way/pattern to have it validate using both the form-specific validations in your form class _and_ the validations in your underlying models? I guess in the simple case (if you're relying on the behavior of if form.validate(params[:key]) and form.save
redirect ...
else
render 'form'
end but I'm worried that it would become ugly if you tried to use the block form... |
About |
@TylerRick, ha, I can't believe you just asked how we deal with the underlying model validations after the form validations. This is exactly the discussion we've just had in our team this afternoon when I presented my new work and refactoring I have done. We actually have 3 layers where validations may exist, here is some pseudo code: form_object.validate(params) # first validation
model.attributes = form_object.to_hash # validation errors could occur here because not covered by the form object
my_service.update_model(form_object.to_hash) # this could fail for other reasons and would too have errors I have developed a simple pattern that copies errors from the model or the service into the form object so that the view can still use those errors and the form object becomes ubiquitous, however it's a bit messy and very domain specific. Do you think think it would be worth me writing up a simple Gem that could handle this all in a nice generic fashion? I think so... |
Good points, @GCorbel, I completely agree. I like your API here much better than reform's API. In your API, you do these as two separate steps: form.fill_attributes(hash)
if form.valid?
if form.save # save all models
redirect_to … whereas reform conflates these two responsibilities into a single if form.validate(hash)
if form.save
redirect_to … I also like how your API ( I would suggest you rename resource.assign_attributes(hash)
if resource.valid?
resource.save
redirect_to … Also, I'm curious, does calling form.assign_attributes(hash)
if form.save
redirect_to … I really like your API in ActiveForm. I may consider switching from Reform to ActiveForm... @apotonick, what do you think of @GCorbel's comments about the single responsibility principle? Would it be all right if we completely got rid of |
@TylerRick if you check the implemention of the I totaly agree with I am also intersted by the opinion of @apotonick. He did a great work and his experience can be very useful. |
@mattheworiordan, I think that would be very useful. I too was wondering how I would roll up all of the errors from my model objects back into the form object so that I can actually show the errors in the view next to the appropriate fields. Do you think this should actually be solved in Reform? There is already a one-to-one mapping between fields in the form and the fields in your model objects. Reform can already sync values between the model object and form object (in both directions, I believe, using I'm guessing this would probably belong in form/active_record.rb. Then we could change the default save behavior for ActiveRecord-backed forms be to save and if the save fails, merge its errors back into the form`s. Maybe something like this? def save(*)
super.tap do
return if block_given?
model.save.tap do |result|
unless result
merge_errors_from(model)
end
end
end
end (Also noticed this merge! method. Not sure if it's helpful or not.) As for copying errors from the service into the form object, that probably wouldn't/couldn't be done in Reform, since it doesn't know anything about your service objects. So maybe you could still write a gem for that portion...? |
The If you use I agree, the The reason Reform does updating attributes and validation in the same step is because I wanna reduce public methods. This is to save users from having to remember state. There are multiple tickets for this and as soon as I finished the representable 1.8 upgrade I will give Reform some more ❤️ API-wise, this gem here basically copies all the concepts found in Reform and puts some Rails sugar on top - I am not saying this is bad but I wonder why we don't incorporate that in Reform itself. |
The reason why I created another gem was just because it was simpler and faster to do it. I can not wait to see the next version of reform. Thanks for your great job. |
I apologize for the slow development of Reform after the "explosion" when I released it initially. The reason for this is I changed jobs and didn't use Reform (yet).Thanks for all your patches and ideas so far!!!!!!!!!!!11one |
No problem. I can understand. This is offtopic but why do you change your job because of a open-source project? |
Haha, I meant to say after I published Reform I changed jobs (unrelated to Reform) and in the new project, we don't use Reform, yet. |
@TylerRick, glad to hear you think it could be useful. I was thinking a DSL that was not connected to any particular class, but would perhaps only expect that any model would respond to form.assign_attributes(hash)
form.sync_and_validate_with(form.model).then_validate do
my_custom_service.update(form.model, form.attributes)
end
if !form.valid?
# render view with form object and errors on the form
end So the above would validate all objects passed into The My only concern with this approach is that if someone calls As such, I'd be inclined to change the behaviour of the predicate What are your thoughts on this? |
@mattheworiordan What if there is a form with multiple models. How do you manage this? |
@GCorbel, what do you mean it does not respect the law of demeter. I kept it simple intentionally, but for a Form with multiple models and multiple return values, you could easily modify as follows: form.assign_attributes(hash)
form.sync_and_validate_with(form.person, form.place, form.company).then_validate do
result = my_custom_service.update(form.model, form.place, form.company, form.attributes)
[result, result.model, result.place, result.company] # pass in array of objects that have errors copied to the form
end
if !form.valid?
# render view with form object and errors on the form
end My concern with this approach is still that it's somewhat brittle with the current implementation of |
@mattheworiordan there is two Furthermore, you call I don't like the fact you must to return an array and you probably must return it in the right order. I agre with your concern. I realy prefer to do this :
It looks more like a normal controller. |
@GCorbel, In your code example, where are the models validated and how do the validations pass to the form object? Secondly, if the service fails, how do the errors get presented to the user, especially if they are inline errors? I am not really following your code example in the context of what we're discussing. |
The model is passed on the creation of the form like this Why a service would fail if the form is valid? If it fail, it will throw an error. |
I've been following this discussion with interest. The difference between the two gems is: Reform is a class with a minimized number of entry points (aka public methods), whereas you guys are designing a DSL. DSLs can be problematic for the user since the user has to manage state (e.g. am I supposed to call
This is wrong. SRP means your class does exactly one thing, which is reflected in a single public method. The more methods you expose, the less SRP you go. Trust me, I thought a lot about What was the original problem with that method in the first place? |
I updated the README - is it ok when I link to this discussion? Might be a bit off-topic.... |
@apotonick, fine by me to link here if you think it's useful. @GCorbel, in response to your points, I am still not following in your example how validation errors of even problems on attributes can be passed back to the form without doing it explicitly in the controller code. I am in no way suggesting ActiveForm-Rails or Reform should provide this DSL, I think it would most likely be wrong to bake this type of functionality into a form object, however I do think it's a common pattern that should be solved, and I am probably going to try and solve it as a Gem as opposed to simply writing code that we use in our code base. Goal: In a controller, use a single consistent Form object in the view for any action Example: class AccountService
include ActiveModel::Validations
def self.create(account)
errors.add(:email, 'Email does not exist on 3rd party') if 3rdPartyService.validate_email(account.email)
account.save! if account.valid?
end
end
class AccountController
def new
@account = AccountForm.new(Account.new)
end
def create
@account = AccountForm.new(Account.new)
@account.update_attributes(params[:account])
@account.sync_and_validate_with(:model).then {
AccountService.create(@account.model)
}.and_check_valid
redirect_to account_path(@account) if @account.valid?
end
end What this solves
I suspect we're not really going to agree on a style we both like. It's been a good debate and I think I'm still keen to solve the problems outlined above. I will when I have a chance see if I can knock something together. |
@mattheworiordan, with ActiveForm-Rails, validations is the responsability of the form and not of the models. There is no need to synchronize errors from the form to the models and vice versa. We have 3 methods to resolve the same problem. It's a very interessant debate with no final anwser. Like all best practices, I think the way you will resolve a problem will depend of the application you are doing. I close the issue but we can continue the discussion. |
@mattheworiordan Did you by any chance ever finish/publish your solution for this? (the code that provides I'm currently looking for/at form object libraries and re-evaluating which one to use, and I resonated with most of the ideas you shared so would be interested in seeing what you use these days (if anything)... I'd like to move away from Reform and towards something that more directly reuses standard ActiveModel modules... like https://github.com/oozou/active_form/blob/master/lib/active_form/base.rb , for instance |
@GCorbel, I was pretty excited when I saw your form object the other day, so integrated at the earliest opportunity. Thanks for contributing it to the community. However, as you can see, I've hit a number of issues using it and have done what I can to help by submitting my two pull requests #2 and #1.
I have now gone home and started wondering if I made the right choice changing from our home grown object models to this gem simply because it's turned out to be a lot more work than I thought. I then did a bit of searching too and came across https://github.com/apotonick/reform.
I'd like to know specifically what you were aiming to achieve with this Gem as opposed to simply using https://github.com/apotonick/reform? I am happy to help contribute, but equally if there is a gem out there that already does the job well, I'd like to know why we shouldn't just use that.
The text was updated successfully, but these errors were encountered: