An Interest In:
Web News this Week
- April 19, 2024
- April 18, 2024
- April 17, 2024
- April 16, 2024
- April 15, 2024
- April 14, 2024
- April 13, 2024
Form validation in Rails
I've seen and implemented many ways to validate a form. Be it replacing a portion of the form with a Turbo stream or by using the more traditional approach of looping through the errors and then displaying the validation messages from the erb
template itself. Although these techniques do the job, I feel that it violates common user experiences. I'll not be diving into the violations because this isn't the scope of this article. Instead, I'll be sharing my approach on how I validate forms and render server errors if any.
Tools used
Outlining the steps
When we submit a form in Rails, Turbo fires numerous events, such as turbo:submit-start
, turbo:submit-end
, and so on (Full list of events can be seen here). What we're interested in is the turbo:submit-end
event that Turbo fires after the form submission-initiated network request is complete.
We'll tap into the event and look for any server-side errors. If there are errors, then we'll display them with the help of Stimulus.js without refreshing the page and without replacing the entire form.
Approach
Assume that we are validating a user sign-up form. We'll first set up our User
model, then UsersController
, then we'll move onto the form
, and then lastly we'll write some Stimulus.js code.
# app/models/user.rbclass User < ApplicationRecord validates :name, presence: true validates :email, presence: true, uniqueness: true validates :password, presence: true, length: { minimum: 6 }end
I know this isn't ideal for validating a user
, but for the sake of this tutorial, it should work.
# app/controllers/users_controller.rbclass UsersController < ApplicationController def new @user = User.new end def create user = User.new(user_params) if user.save # do something else render json: ErrorSerializer.serialize(user.errors), status: :unprocessable_entity end end private def user_params params.require(:user).permit(:name, :email, :password) endend
I'd like you all to focus on the else
block of the create
action. We want to send a JSON
response of all the errors that are triggered in a format that can be easily consumed by our Stimulus controller.
errors = [ { type: "email", detail: "Email can't be blank" }, { type: "password", details: "Password is too short (mininum is 6 characters)" }]
This is the format of the errors
that we want. Let's define the ErrorSerializer
module to render this format.
# app/serializers/error_serializer.rbmodule ErrorSerializer class << self def serialize(errors) return if errors.nil? json = {} json[:errors] = errors.to_hash.map { |key, val| render_errors(errors: val, type: key) }.flatten json end def render_errors(errors:, type:) errors.map do |msg| normalized_type = type.to_s.humanize msg = "#{normalized_type} #{msg}" { type: type, detail: msg } end end endend
Now that we've covered the backend portion, let's move onto implementing the form and the Stimulus.js controller.
<%# app/views/users/new.html.erb %><%= form_with model: @user, data: { controller: "input-validation", action: "turbo:submit-end->input-validation#validate" } do |f| %> <div> <%= f.label :name %> <%= f.text_field :name %> <p data-input-validation-target="errorContainer" data-error-type="name" role="alert"></p> </div> <div> <%= f.label :email %> <%= f.email_field :email %> <p data-input-validation-target="errorContainer" data-error-type="email" role="alert"></p> </div> <div> <%= f.label :password %> <%= f.password_field :password %> <p data-input-validation-target="errorContainer" data-error-type="password" role="alert"></p> </div> <%= f.submit "Sign up" %><% end %>
Notice the p
tag. We've added the data-error-type
by which we'll know where to show the errors for a particular field. Next, we'll write some javascript to insert the errors in the respective p
tags.
// app/javascript/controllers/input_validation_controller.jsimport { Controller } from "stimulus"export default class extends Controller { static targets = ['errorContainer'] async validate(event) { const formData = await event.detail.formSubmission const { success, fetchResponse } = formData.result if (success) return const res = await fetchResponse.responseText const { errors } = JSON.parse(res) this.errorContainerTargets.forEach((errorContainer) => { const errorType = errorContainer.dataset.errorType const errorMsg = extractError({ errors, type: errorType }) errorContainer.innerText = errorMsg || '' }) }}function extractError({ errors, type }) { if (!errors || !Array.isArray(errors)) return const foundError = errors.find( (error) => error.type.toLowerCase() === type.toLowerCase() ) return foundError?.detail}
When we submit the form, turbo:submit-end
event gets fired calling in the validate
function in the Stimulus.js controller. If the validation succeeds then we do an early return
, else we destructure the errors and render them inside the p
tag that we defined earlier in our sign-up form.
This is one of the many ways to render errors on the client-side. If you have a better implementation, please let us know in the comments. Also, if you'd like to read a particular topic, do let me know.
Original Link: https://dev.to/abeidahmed/form-validation-in-rails-56dc
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To