Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 14, 2021 08:44 am GMT

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

  1. Turbo
  2. Stimulus.js

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

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To