rocket_pants icon indicating copy to clipboard operation
rocket_pants copied to clipboard

Displaying validation errors in client application forms

Open artem-mindrov opened this issue 11 years ago • 0 comments

Hi Darcy, Thanks for the handy gem. I've got one question I couldn't find an easy solution for, maybe you can help me out with this. I have two separate applications (one for regular users, another administration) with a single user base, the admin app acting as a web service for the user app (the API client). I need to display validation errors on login/registration forms in the user app. Since the user app is not backed by a DB, things become a little unwieldy on the client side since I don't get any of the ActiveModel convenience stuff out of the box when using RP clients instead of ActiveRecord models. Here's what I had to do to get the errors displayed properly.

I defined base classes for clients and the hashes they encapsulate (they are later used to trick form builder into binding input fields to attributes). The encapsulated 'model' has to have a model_name method and an ActiveModel error hash, otherwise simple_form_for will die with exceptions I've spent about 3 hours debugging :(

module Api
  class Base < RocketPants::Client
    version 1
    base_uri Rails.application.config.api.base_uri

    attr_accessor :auth_token

    def base_request_options
      session[:user] ? { headers: { 'X-AUTH' => session[:user][:auth_token] } } : {}
    end
  end

  class ModelBase < APISmith::Smash
    property :id
    property :errors

    def self.model_name
      ActiveModel::Name.new(self)
    end

    def to_key
      id
    end

    def populate_errors_from(error_hash)
      self.errors = ActiveModel::Errors.new(self)

      error_hash.each do |attr, msgs|
        msgs.each { |msg| self.errors.add(attr, msg) }
      end
    end
  end
end

Now, a client for the User model:

class Api::UserClient < Api::Base
  class User < Api::ModelBase
    property :first_name
    property :last_name
    property :email
    property :password
    property :password_confirmation
    property :provider_id
  end

  attr_reader :user

  def initialize(*args)
    @user = User.new(*args)
    super(*args)
  end

  def find(id)
    get "/user", extra_query: { id: id }
  end

  def save
    post "/users", extra_query: { user: @user }
  end
end

The corresponding controller on the client application side is below. It rescues the invalid record exception raised by the client base code, populates the error hash and re-renders the action with the form.

class UsersController < ApplicationController
  skip_before_filter :authenticate, only: [:new, :create]

  def create
    @user_client = Api::UserClient.new(params[:user])

    respond_to do |format|
      begin
        @user_client.save
      rescue RocketPants::InvalidResource => e
        format.html do
          @user = @user_client.user
          @user.populate_errors_from(e.errors)
          render action: :new
        end
      else
        format.html { redirect_to root_path, notice: t("signed_up_but_not_approved") }
      end
    end
  end
end

This works, but feels way too convoluted for a simple scenario like this. I feel there should be an easier way to do this. I guess this post is a little too long, but anyway I'd be grateful if you could read through this and point me in the right direction here. I hope I've explained everything clear enough.

Thanks.

artem-mindrov avatar Feb 28 '13 20:02 artem-mindrov