Skip to content

Backend Training: Form Helper

Nelson Lee edited this page Nov 30, 2017 · 3 revisions

Next, from the mock up we need a contact page with a form for the visitor to submit inquiries to us.

Setup pages

We already have a contact model from previous section so let's add a controller for contact.

rails g controller contacts

Go to app/controllers/contacts_controller.rb and add..

# app/controllers/contacts_controller.rb
class ContactsController < ApplicationController

  def new; end

  def create; end

end

And let's add routes for contacts

# config/routes.rb
Rails.application.routes.draw do
  devise_for :admin_users, ActiveAdmin::Devise.config
  ActiveAdmin.routes(self)
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  %w[404 401 500 503].each do |code|
    get code, to: 'errors#show', code: code
  end

  resources :faqs,     only: %i[index]
  resources :posts,    only: %i[index show]
  resources :contacts, only: %i[new create]

  get '/pricing' => 'home#pricing', as: :pricing

  root 'home#index'
end

p.s. here we are using the new and create actions because new is initializing a new object and create is creating an object in standard CRUD actions

Let's verify that the routes have been added properly

$ rails routes | grep contacts
contacts POST       /contacts(.:format)        contacts#create
new_contact GET     /contacts/new(.:format)    contacts#new

Great we have the contact routes.

Next, lets add a new template for the new action

# app/views/contacts/new.slim
section.jumbotron[id='hero']
  .container
    .row.text-center
      .col-md-12
        h1= t('.hero.title')

section[id='contact-info']
  .container
    .row
      .col-md-12
        h2= t('.contact_info.title')
    .row
      .col-md-5
        p= t('.form.description')
      .col-md-7
        strong= t('.info.contact.title')
        ul
          li
            = fa_icon('phone')
            | &nbsp;
            = t('.info.contact.phone')
          li
            = fa_icon('envelope')
            | &nbsp;
            = t('.info.contact.email')
          li
            = fa_icon('map-marker')
            | &nbsp;
            = t('.info.contact.address')
        br/
        strong= t('.info.hours.title')
        p== t('.info.hours.body')

Add translations

# config/locales/en.yml
en:
  ......

  contacts:
    new:
      hero:
        title: Contact Us
      contact_info:
        title: Let’s Get Started!
      form:
        description: Ready to talk about your project? Need a quote or just want to bounce ideas off us? We’d love to help. Bring us your ideas, lists or drawings - we’ll put the kettle on.
      info:
        contact:
          title: Contact Information
          phone: (778) 806-1108
          email: Email Us
          address: 5050 Kingsway, Unit 412, Burnaby, BC, V5H4H2
        hours:
          title: Office Hours
          body: Monday-Friday, 9:30AM - 5:30PM </br> We are closed on Canadian Holidays

Let's go to /contacts/new and you should get something like...

contact#new

Great! Let's add a link to this page on the navbar.

# app/views/components/_navbar.slim
nav.navbar.navbar-default
  .container
    .navbar-header
      ......
    .collapse.navbar-collapse[id='main-nav']
      ul.nav.navbar-nav.navbar-right
        ......
        li= link_to t('.contact'), new_contact_path

And translation for it

# config/locales/en.yml
en:
  ......

  components:
    navbar:
      home: HOME
      about: ABOUT
      pricing: PRICING
      pricing/basic: Basic Package
      pricing/advance: Advance Package
      faqs: FAQS
      posts: NEWS
      contact: CONTACT

  ......

Refresh the browser Command ⌘+r, and the link should show up.

Add forms

🔖 : Form Helper, simple_form, rails-i18n

Form helper works well with ActiveRecord Model objects so let's add a new record to the controller.

# app/controllers/contacts_controller.rb
class ContactsController < ApplicationController

  def new
    @contact = Contact.new
  end

  def create; end

end

Now, we have the object so lets go ahead and add the form.

# app/views/contacts/new.slim
section.jumbotron[id='hero']
  .container
    .row.text-center
      .col-md-12
        h1= t('.hero.title')

section[id='contact-info']
  .container
    .row
      ......
    .row
      .col-md-5
        p= t('.form.description')
        = simple_form_for @contact do |f|
          = f.input :name
          = f.input :email
          = f.input :message
          = f.button :submit
      .col-md-7
        ......

Refresh the browser Command ⌘+r, and you should see something like...

contact#new

Great! We have a form but our form doesn't exactly look like the one on the mockup. Let's change it to reflect the mockup.

# app/views/contacts/new.slim
section.jumbotron[id='hero']
  .container
    .row.text-center
      .col-md-12
        h1= t('.hero.title')

section[id='contact-info']
  .container
    .row
      ......
    .row
      .col-md-5
        p= t('.form.description')
        = simple_form_for @contact do |f|
          = f.input :name,
                    label: false
          = f.input :email,
                    label: false
          = f.input :message,
                    label: false,
                    input_html: { rows: 5 }
          = f.button :submit,
                     class: 'btn-primary'
      .col-md-7
        ......

Let's add translations for the forms.

The submit button translation is using rails default form helper translations so we need to change it by overriding it under helpers. For complete list of rails default translations please see doc

# config/locales/en.yml
en:
  helpers:
    submit:
      contact:
        create: Submit

  .......

The placeholders for the fields are using simple_form translations so let's add them inside simple_form.en.yml

# config/locales/simple_form.en.yml
en:
  simple_form:
    "yes": 'Yes'
    "no": 'No'
    required:
      ......
    error_notification:
      ......

    placeholders:
      contact:
        name: Full Name*
        email: Email Address*
        message: Message

    # Examples
    .......

Refresh the browser Command ⌘+r, and you should see something like...

contact#new

Great! Our form now looks a lot like the mockup.

Creating record

🔖 'Strong Params' Next let's hit the create button and see what happens. From your server logs you should see...

Started POST "/contacts" for 127.0.0.1 at 2017-11-30 12:41:45 -0800
Processing by ContactsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"chmrIP70VaNwnTrwH0yw+AnpDOqnkz6YD6TEsPZD2wDTEqWIQ7URj3Rx57D1mj5oQpis3pA4/+WBLtdCFhJ8vw==", "contact"=>{"name"=>"", "email"=>"", "message"=>""}, "commit"=>"Submit"}
No template found for ContactsController#create, rendering head :no_content
Completed 204 No Content in 383ms (ActiveRecord: 0.0ms)

Looks like it is going to the create action inside contacts_controller. Let's modify our create controller so we can create an object. Here we need use of Rails' Strong Params as part of the security measure to allow only the params we whitelisted. Let's also add a debugger to fully understand the logic behind this.

# app/controllers/contacts_controller.rb
class ContactsController < ApplicationController

  def new
    @contact = Contact.new
  end

  def create
    debugger
    @contact = Contact.new contact_params
  end

  private

  def contact_params
    params.fetch(:contact, {}).permit(
      :name, :email, :message
    )
  end

end

Ok, let's clear out the logs by pressing Command+k in your server terminal, and click on the submit button again.

You should see something like inside your server terminal.

[4, 13] in /Users/ilunglee/Work/rails/mock_project/app/controllers/contacts_controller.rb
    4:     @contact = Contact.new
    5:   end
    6:
    7:   def create
    8:     debugger
=>  9:     @contact = Contact.new contact_params
   10:   end
   11:
   12:   private
   13:
(byebug)

Great! Let's try debugging this part of the code.

(byebug) params
<ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"B3HtQGAPohBb0cVmXpAOG2E/CSSX7NSEARXcyWu/OLemeuPo3U7mPF89GCa0RoCLKk6pEKBHFfmPn887i+6fCA==", "contact"=>{"name"=>"", "email"=>"", "message"=>""}, "commit"=>"Submit", "controller"=>"contacts", "action"=>"create"} permitted: false>

params gives us all the parameters send from the browser to the server, and we see that all the contact form parameters are wrap inside the contact hash "contact"=>{"name"=>"", "email"=>"", "message"=>""}.

(byebug) params.fetch(:contact)
<ActionController::Parameters {"name"=>"", "email"=>"", "message"=>""} permitted: false>

params.fetch(:contact) gives us all the parameters inside contact hash.

(byebug) params.fetch(:contact, {})
<ActionController::Parameters {"name"=>"", "email"=>"", "message"=>""} permitted: false>

params.fetch(:contact, {}) gives us all the parameters inside contact hash as well but will return empty hash {} if contact is not found instead of returning errors.

Let's try this by fetching a non-existing parameter

(byebug) params.fetch(:post)
*** ActionController::ParameterMissing Exception: param is missing or the value is empty: post

nil

params.fetch(:post) throws an error because post does not exist inside the parameters.

(byebug) params.fetch(:post, {})
<ActionController::Parameters {} permitted: false>

params.fetch(:post, {}) returns an empty hash instead of error.

Great now we understand this part, next, we should investigate on the permitted attribute. From the above examples you should see that in every params log it all shows permitted: false. This is cause by Rails strong parameters to discard any parameters that are not explicitly allowed by the server.

To fix this we will add our parameters to the whitelist by...

(byebug) params.fetch(:contact, {}).permit(:email)
Unpermitted parameters: name, message
<ActionController::Parameters {"email"=>""} permitted: true>

Now, we see that email is permitted but it also shows Unpermitted parameters: name, message. Let's fix this by add all of them to permit method.

(byebug) params.fetch(:contact, {}).permit(:name, :email, :message)
<ActionController::Parameters {"name"=>"", "email"=>"", "message"=>""} permitted: true>

Great! All our parameters are permitted by the controller. Type c to continue.

(byebug) c
No template found for ContactsController#create, rendering head :no_content
Completed 204 No Content in 783198ms (ActiveRecord: 0.0ms)

Let's move on to add the functionality to save contact inside the create action.

# app/controllers/contacts_controller.rb
class ContactsController < ApplicationController

  def new
    @contact = Contact.new
  end

  def create
    @contact = Contact.new contact_params
    @contact.save
    render 'new'
  end

  ......

end

Refresh the browser Command ⌘+r, and click on submit again... contact#create

Great! Our form is working. Let's commit and move on to the next section. p.s. Fix all the overcommit errors and commit

$ git add -A
$ git commit -m 'Contact Form Setup'

↪️ Next Section: Flash Message

Clone this wiki locally