Construindo seu primeiro formulário com tratamento de erros no Ruby on Rails 7

Construindo seu primeiro formulário com tratamento de erros no Ruby on Rails 7

Seja qual for a plataforma web que queremos desenvolver, os formulários são encontrados em toda parte. Estão presentes no cadastro de usuário para login, no preenchimento de dados de pagamento para compra de um produto, ou qualquer que seja a entrada de dados do usuário no produto.

Nesse tutorial iremos fazer juntos um formulário em Ruby on Rails, utilizando a biblioteca de estilização Bootstrap e o framework JavaScript Stimulus. Aplicaremos validações nos campos preenchidos e apresentaremos os erros de preenchimento nos respectivos campos.

Nossa plataforma web será composta por três páginas:

  • Uma página apresentando usuários cadastrados
  • Uma página com formulário de cadastro para novos usuários
  • Uma página com formulário para edição de cadastros existentes

Do que você precisa na sua máquina?

Esse tutorial foi elaborado utilizando sistema operacional Ubuntu, caso esteja utilizando outro sistema operacional, basta adaptar os scripts do terminal para seu caso de uso. Antes de começar, verifique se os seguintes pacotes estão instalados:

  • Ruby on Rails 7.0.2.3;
  • Ruby 3.0.0;
  • npm 8.5.0;
  • node 16.14.2
  • yarn 1.22.18

No terminal, vamos criar nosso projeto com a configuração do bootstrap. Através do comando:

rails new sample-app --css bootstrap

Ao abrir nosso repositório recem gerado pelo rails, confira se o arquivo package.json também foi gerado e está com os pacotes adicionados corretamente. Seu arquivo deve se parecer com esse:

{
  "name": "app",
  "private": "true",
  "dependencies": {
    "@hotwired/stimulus": "^3.0.1",
    "@hotwired/turbo-rails": "^7.1.1",
    "@popperjs/core": "^2.11.4",
    "bootstrap": "^5.1.3",
    "bootstrap-icons": "^1.8.1",
    "esbuild": "^0.14.27",
    "sass": "^1.49.9"
  },
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
}

Depois disso, devemos criar nosso banco de dados utilizando o comando:

rails db:create

A essa altura já podemos subir nosso servidor local para ver nossa aplicação tomando forma. Utilizando o comando rails sou rails servere acessando a url localhost:3000 veremos nossa primeira página web, ainda com o “hello-world” do Rails.

Criando nosso primeiro formulário

Antes de testar se nossa biblioteca frontend está funcional, vamos primeiro criar o modelo, view e controller do nosso recurso: um usuário simples com campos mais utilizados em formulários de cadastro do Brasil (nome, email, CPF, telefone e data de nascimento).

rails generate scaffold User name:string phone:string email:string cpf:string birthdate:date
rails db:migrate

Altere seu arquivo routes.rb para direcionar a primeira página para a lista de usuários:

Rails.application.routes.draw do
  resources :users
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  root "users#index"
end

Depois dessas modificações teremos um formulário totalmente funcional, ou seja, podemos visualizar dados de usuário cadastrados, cadastrar um novo usuário, visualizar, editar e excluir.

Nosso formulário pouco se parece com as páginas que vemos nas plataformas do nosso dia-à-dia, isto porque não possuem ainda nenhum tipo de estilização CSS ou scripts em javascript que melhoram e experiencia de usuário.

Na etapa anterior, fizemos a configuração inicial do projeto em Rails com instalação da biblioteca Bootstrap, mas será que temos nossa biblioteca Bootstrap realmente instalada e pronta pra uso?

Aplicando Bootstrap em nosso formulário

Nessa etapa iremos adicionar classes CSS à nossas páginas de formulário. Não sou nenhuma designer, mas irei fazer modificações simples seguindo como inspiração o design adotado pela plataforma Github tornando os formularios mais elegantes. Fique a vontade para sair fora-da-caixa e testar outros estilos.

  • Comece pelo layout: no seu arquivo application.html.erb, adicione elementos padrões como navbar e container. Dentro de assets/images adicione uma imagem para o logo da navbar.
...
<body>
  <nav class="navbar navbar-dark bg-dark py-3 px-4">
    <%= image_tag 'logo' %>
  </nav>
  <div class="container">
    <%= yield %>
  </div>
</body>
...
  • Apresente listas em formato de tabela: no nosso exemplo, vamos fazer isso para a lista de usuários editando o arquivo views/users/index.html e views/users/_user.html.erb. Como os dados do nosso usuario são poucos e podem ser apresentados em uma unica linha, podemos remover a view de :show
  • Em nosso formulario de cadastro, alem de estilizar os campos utilizando a classe css form-control, vamos agrupar os campos relacionados para facilitar o preechimento do nosso usuário.

Aplicando mascaras Javascript em nosso formulário

Outra melhoria simples que podemos fazer no nosso formulário, é adicionar máscaras de formatação nos campos de CPF e e telefone. As mascaras facilitam o preenchimento de campos, fazendo com que o usuário complete os campos exatamente no padrão que desejamos.

Procurei bastante, por algum plugin de máscara que não fosse dependente da biblioteca Jquery, mas não encontrei nenhuma opção. Indico o plugin jquery-mask-plugin que irá facilitar muito nosso trabalho nesse quesito. Para fazer a instalação basta executar o comando no terminal:

yarn add jquery
yarn add jquery-mask-plugin

O Rails 7 já vem com o framework JavaScript Stimulus instalado por padrão, recomendo ao leitor que em outro momento leia a documentação completa, ela ajudará a entender profundamente como utilizar o framework.

Em poucas palavras, adicionaremos nosso script em um arquivo controller. Os arquivos de controllers estão organizados na pasta app/javascript/controllers/e nela adicionaremos o arquivo:

window.$ = window.jQuery = require('jquery');
require("jquery-mask-plugin");

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.maskFields();
  }
  
  maskFields() {
    $('[data-masks-target="phone"]').mask('(00) 00000-0009');
    $('[data-masks-target="cpf"]').mask('000.000.000-00');;
  }
}

Importaremos esse script na view do nosso formulário de cadastro de usuário (arquivo app/views/users/_form.html.erb). E adicionaremos atributos data ao campos pertinentes, para que o script encontre os inputs que deve adicionar as máscaras.

<%= form_with(model: user, html: { data: { controller: "masks" } }) do |form| %>
...
      <div class="row">
        <div class="col">
          <%= form.label :cpf, style: "display: block" %>
          <%= form.text_field :cpf, class: "form-control", data: { masks_target: 'cpf' } %>
        </div>
        ...
        <div class="col">
          <%= form.label :phone, style: "display: block" %>
          <%= form.text_field :phone, class: "form-control", data: { masks_target: 'phone' } %>
        </div>
      </div>
...

Adicionando validações

Desejamos adicionar algumas validações de preenchimento para esse formulário:

  • Todos os campos devem ser obrigatórios
  • Os campos de CPF e email devem ser válidos

Para a validação de preenchimento obrigatorio, podemos simplemente fazer uso das validações do Active Record no model de user:

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

A biblioteca cpf_cnpj cuidará das validações de veracidade do campo de CPF e faremos um método customizado com regex para validar o email. Começaremos pela instalação da gem executando o comando no terminal:

gem install cpf_cnpj

Faça as modificações necessárias no arquivo models/user.rb para ficar como desejamos:

class User < ApplicationRecord
  validates :name, :phone, :email, :cpf, :birthdate, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  validate :cpf_valid?

  def cpf_valid?
    return if CPF.valid?(cpf)

    errors.add(:cpf)
  end
end

Caso o usuário preencha esses dados incorretamente, os erros são apresentados na tela de criação/edição de usuário. Será que conseguimos melhorar a apresentação desses erros?

Melhorando nosso feedback de erros de preenchimento

Podemos melhorar o feedback de erros de prenchimento do nosso formulário mostrando o erro de cada campo logo abaixo do seu respectivo input.

Antes de qualquer coisa, vamos analisar como o ActiveRecord estrutura os erros de validação do model. Para isso, vamos instalar uma ferramenta de debug muito conhecida: Pry. Para instalar basta abrir o terminal novamente e executar:

gem install pry

Reinicie seu servidor. Adicione o “breakpoint” no controller, na action :create para averiguarmos os erros no terminal no momento em que preenchemos dados errados no formulário:

  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to user_url(@user), notice: "User was successfully created." }
        format.json { render :show, status: :created, location: @user }
      else
        binding.pry
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

Acesse o formulário na url http://localhost:3000/users/new e clique em salvar. Depois vá até o terminal e execute@users.errors. Os erros serão apresentados no seu terminal:

Concluímos que o ActiveRecord estrutura os erros de validação do model com a classe ActiveModel::Errors , que tem como atributos: nome do input (attribute) e tipo de erro (type). Assim, se queremos obter os erros do campo :namepodemos simplesmente acessar através de:

@user.errors[:name]

E, mais genericamente, podemos acessar os erros de qualquer objeto através de:

object.errors[field]

Com essa descoberta, podemos adicionar um método ao nosso helper que receba dois parametros: form e field, e passaremos esses parametros através da view.

module ApplicationHelper
  def errors_for(form, field)
    tag.p(form.object.errors[field].try(:first), class: 'invalid-feedback')
  end
end

Repare que, em nosso método, chamamos uma classe css chamada “invalid-feedback”; precisamos customizar o css dessa classe, bem como customizar as bordas do nosso formulário. Podemos fazer isso adicionando o arquivo form.scsse importá-lo no app/assets/stylesheets/application/bootstrap/scss

.field_with_errors .form-control{
  border-color: $danger;
}

.invalid-feedback {
  display: block;
  color: $danger;
}

O código do nosso formulário final ficará assim:

<%= form_with(model: user, html: { data: { controller: "masks" } }) do |form| %>
  <div class="row">
    <div class="col-6 card p-3 class">
      <div class="row mb-2">
        <div class="col">
          <%= form.label :name, 'Nome', style: "display: block" %>
          <%= form.text_field :name, class: "form-control" %>
          <%= errors_for(form, :name) %>
        </div>
      </div>
      <div class="row mb-2">
        <div class="col">
          <%= form.label :cpf, 'CPF', style: "display: block" %>
          <%= form.text_field :cpf, class: "form-control", data: { masks_target: 'cpf' } %>
          <%= errors_for(form, :cpf) %>
        </div>
        <div class="col-6">
          <%= form.label :birthdate, 'Data de nasc.', style: "display: block" %>
          <%= form.date_field :birthdate, class: "form-control" %>
          <%= errors_for(form, :birthdate) %>
        </div>
      </div>
      <div class="row mb-2">
        <div class="col">
          <%= form.label :email, style: "display: block" %>
          <%= form.text_field :email, class: "form-control" %>
          <%= errors_for(form, :email) %>
        </div>
        <div class="col">
          <%= form.label :phone, 'Celular', style: "display: block" %>
          <%= form.text_field :phone, class: "form-control", data: { masks_target: 'phone' } %>
          <%= errors_for(form, :phone) %>
        </div>
      </div>
      <div class="mt-3">
        <%= form.submit 'Salvar', class: "btn btn-primary text-white" %>
      </div>
    </div>
  </div>
<% end %>

E nosso objetivo foi concluído. Estamos apresentando os erros de preenchimento embaixo dos respectivos campos:

Melhorando o feedback ao salvar o usuário

Quando salvamos nosso usuário com sucesso no banco de dados, por padrão do Rails, somos direcionados para a tela de detalhes (show) desse usuário. No passo 3 desse tutorial, sugeri de removermos essa action. Nesse caso precisamos alterar as actions de :create e :update para redirecionar para a tela de :index em caso de sucesso. Nosso código no controller ficará assim:

class UsersController < ApplicationController
  before_action :set_user, only: %i[ edit update destroy ]

  # GET /users or /users.json
  def index
    @users = User.all
  end

  # GET /users/new
  def new
    @user = User.new
  end

  # GET /users/1/edit
  def edit
  end

  # POST /users or /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to users_path, notice: "Usuário foi criado com sucesso" }
        format.json { render :index, status: :created, location: @user }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /users/1 or /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to users_path, notice: "Usuário foi editado com sucesso" }
        format.json { render :index, status: :ok, location: @user }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /users/1 or /users/1.json
  def destroy
    @user.destroy

    respond_to do |format|
      format.html { redirect_to users_url, notice: "User was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.require(:user).permit(:name, :phone, :email, :cpf, :birthdate)
    end
end

Adicione o código no arquivo application.html.erb para que as notificações apareçam sempre no topo das suas páginas e remova-o da view :index:

  <body>
    <nav class="navbar navbar-dark bg-dark py-3 px-4 mb-5">
      <%= image_tag 'logo' %>
    </nav>
    <div class="container">
      <% if notice.present? %>
        <p class="alert alert-success" style="color: green"><%= notice %></p>
      <% end %>
      <%= yield %>
    </div>
  </body>

Conclusão

🎉Conseguimos o resultado que queríamos e aprendemos muita coisa em nosso caminho até aqui:

  • Iniciar um projeto novo utilizando Rails 7;
  • Uso do comando Scaffold para gerar o cadastro completo de usuários;
  • Uso da biblioteca Bootstrap para melhorar a aparência da nossa plataforma;
  • Aplicação de máscaras no formulário;
  • Utilização das validações de Model utilizando ActiveRecord;
  • Uso de ferramentas de debug: Pry;
  • Criação de métodos helpers para utilização em views.

Caso tenha alguma dúvida, poderá acessar o projeto em: https://github.com/samillamacedo/sample

Próximos passos

Não pare os estudos por aí, podemos ainda aplicar mais melhorias em nosso formulário:

  • Adicionar login utilizando a gem devise;
  • Inserir upload de imagem para upload do Avatar do nosso usuário;
  • Adicionar tradução aos nossos erros e estruturar esses arquivos de tradução;
  • Estruturar o formulário utilizando o design pattern FormObject;
  • Deixar a tela responsiva para uso em diferentes dispositivos (tablet, desktop e mobile)
  • Inserir testes utilizando a biblioteca rspec;
  • Arquivo seed.rb para popular o banco de dados do nosso ambiente de desenvolvimento.