Layered Design for Ruby on Rails Applications by Vladimir Dementyev

Layered Design for Ruby on Rails Applications by Vladimir Dementyev

Author:Vladimir Dementyev
Language: eng
Format: epub
Publisher: Packt
Published: 2023-11-15T00:00:00+00:00


A form object is more than a model wrapper

So far, we have only considered using form objects for interactions involving a single model. But the number of models affected by a user action (such as form submission) could be any whole number N (yes, including zero), and form objects fully awake when N is not equal to one.

Multi-model forms

Let’s recall the example User model from Chapter 4, Rails Anti-Patterns? (in the Active Record callbacks go wild section):

class User < ApplicationRecord after_create :generate_initial_project, unless: :admin? # ... end

We added a callback to create a project record for a user on creation (or registration). Thus, user registration is a multi-model operation, which we encapsulated within a User.create call. This is a perfect candidate for form object extraction, so let’s do that.

We can define our RegistrationForm class as follows:

class RegistrationForm < ApplicationForm attribute :name attribute :email attribute :should_create_project, :boolean attribute :project_name validates :project_name, presence: true, if: :should_create_project attr_reader :user after_save :create_initial_project, if: :should_create_project private def submit! @user = User.create!(email:, name:) end def create_initial_project user.projects.create!(name: project_name) end end

We moved the project creation step into a callback, which is only invoked if a user opts in. Similarly, we added conditional validation for the project name presence. What about user-related attribute validation? We can assume that the model itself validates email and name presence:

class User < ApplicationRecord validates :email, :name, presence: true end

One important feature of a form object is that it should handle invalid input gracefully. Right now, if we try to submit a form without any email or name provided, an exception is raised:

RegistrationForm.new(name: "Test").save #=> ActiveRecord::RecordInvalid: Validation failed: Email can't be blank

One option is to duplicate validations in the form object class. Alternatively, we can delegate validation to a model instance:

class RegistrationForm < ApplicationForm # ... validate :user_is_valid def initialize(...) super @user = User.new(email:, name:) end def user_is_valid return if user.valid? merge_errors!(user) end end

We override the default constructor to create a user instance immediately upon initialization. Then, during validation, we check whether the user object is valid and merge its validation errors into a form object’s errors set otherwise. The #merge_errors! method can be implemented in the ApplicationForm class:

class ApplicationForm # ... def merge_errors! other.errors.each do |e| errors.add(e.attribute, e.type, message: e.message) end end end

Now we can show validation errors to a user:

form = RegistrationForm.new(name: "Test") form.save #=> false puts form.errors.full_messages #=> Email can't be blank

Similarly, we can delegate project attribute validation to the corresponding model. However, we should never do the opposite: add model-level validations required for a specific form object (and, thus, a context).

To finish this section, let’s consider a case of an N model form where N is zero.



Download



Copyright Disclaimer:
This site does not store any files on its server. We only index and link to content provided by other sites. Please contact the content providers to delete copyright contents if any and email us, we'll remove relevant links or contents immediately.