Forms

Lecture Notes for CS 142
Winter 2014
John Ousterhout

  • Goals for forms:
    • User interface for editing data
    • Upload changes to the server
  • Form:
    • Collection of elements, each of which has
      • Name (used by application, often invisible to user)
      • Value (just a string)
      • User interface
    • Different types of elements provide different ways to edit the value (text entry, checkbox, menu, etc.)

HTML Form Support

  • Simple form example:
    <form action="/product/update" method="post">
      Product: <input type="text" name="product"/><br />
      Price: <input type="text" name="price" value="49.95"/><br />
      <input type="submit" value="Submit"/>
    </form>
    
    • <form> element: overall container for form elements
      • action specifies a URL to invoke when the form is submitted.
      • method attribute indicates which HTTP method to use when communicating with the server; defaults to GET but should almost always be POST (GET limits size of posted data)
      • Arbitrary HTML OK inside <form> (e.g., decorations)
      • Can have more than one form in a page
    • <input> elements: controls for entering data.
      • type attribute specifies which of several controls to use. text is a simple one-line text entry.
      • name attribute: used to identify this particular value when posting form to server.
      • value attribute specifies initial value for this element.
    • <input type="submit"> creates a button for submitting the form.
      • value attribute specifies text to display in button.
    • Other form elements: see formElements.html for examples.
  • When the submit button is clicked:
    • HTTP POST request is made to the URL given by the action attribute of the <form> element.
    • Body of the request contains name-value pairs from the <input> elements, URL-encoded (looks like query data in a URL).
    • Result is an HTML page that replaces the form page.
  • On the server side:
    • Like any other HTTP request.
    • Rails makes the form data available through the params hash.

Issues With Forms

  • Problem #1: page flow
    • Consider an order entry system:
      • One or more pages of forms
      • Final form to confirm order
      • "Thank you for your order" page
    • Use Back button to return to the thank-you page
  • Solution:
    • One URL displays form
    • POST goes to a second URL
    • Redirect after POST:
      • POST page doesn't generate HTML
      • After redirection, POST URL disappears from browser history list
  • Divide URLs:
    • Those that display information (including forms) (GET method)
    • Those that invoke modifications (POST method)
    • Don't mix these two: split, with redirection from one to the other
  • Problem #2: error handling
    • Invalid form data detected by server during POST
    • Desired effect:
      • Redisplay the form page
      • Display error messages about the problems (ideally, display messages next to the offending form elements)
      • Retain all of the data that the user entered in the form

Rails form support

  • Ties in nicely to the ORM system
    • One model object holds one database record
    • Form used to edit part or all of a record
    • Form helpers: methods to generate HTML elements for forms: you don't have to write HTML directly.
    • Validation: mechanism for detecting errors in input data.
  • Form helpers example (student record):
    • Controller:
      @student = Student.find(params[:id])
      
    • Simple view:
      <%= form_for(@student, method: :post,
          url: {action: :update, id: @student.id}) do |form| %>
        <%= form.text_field(:name) %>
        <%= form.text_field(:birth) %>
        <%= form.text_field(:gpa) %>
        <%= form.text_field(:grad) %>
        <%= form.submit "Modify Student" %>
      <% end %>
      
    • @student argument to form_for: object whose contents are to be displayed in the form
    • :url argument to form_for: URL where form will get posted
    • form.text_field: returns an HTML input element of type text, provides initial value from corresponding attribute of @student:
      <input id="student_name" name="student[name]" size="30"
              type="text" value="Wendy Wilson" />
      
    • Can also mix other templating stuff in with form elements to control formatting and provide other information:
        <%= form_for(:student, :url => {:action => :update, :id => @student.id}) do |form| %>
          <table class="form">
            <tr>
              <td><%= form.label(:name, "Name:")%><td>
              <td><%= form.text_field(:name) %><td>
            </tr>
            <tr>
              <td><%= form.label(:birth, "Date of birth:")%><td>
              <td><%= form.text_field(:birth) %><td>
            </tr>
            <tr>
              <td><%= form.label(:gpa, "Grade-point average:")%><td>
              <td><%= form.text_field(:gpa) %><td>
            </tr>
            <tr>
              <td><%= form.label(:grad, "Graduation year:")%><td>
              <td><%= form.text_field(:grad) %><td>
            </tr>
          <table>
          <%= submit_tag "Modify Student" %>
        <% end %>
      
  • Handling post for updating record:
    def update
      @student = Student.find(params[:id])
      @student.name = params[:student][:name]
      @student.birth = params[:student][:birth]
      @student.gpa = params[:student][:gpa]
      @student.grad = params[:student][:grad]
      if @student.save then
        redirect_to(:action => :show)
      else
        render(:action => :edit)
      end
    end
    
    • Form data automatically available in params: nested hash named after the form.
  • A more compact approach that doesn't quite work:
    def update
      @student = Student.find(params[:id])
      if @student.update(params[:student]) then
        redirect_to(:action => :show)
      else
        render(:action => :edit)
      end
    end
    
    • Copies all of the permitted form fields from params into the model object, updates model on disk.
    • But, fails because of security issues: some fields shouldn't be settable by users.
  • The Rails 4 preferred pattern:
    def update
      @student = Student.find(params[:id])
      if @student.update(student_params(params[:student])) then
        redirect_to(:action => :show)
      else
        render(:action => :edit)
      end
    end
    
    private
    def student_params(params)
      return params.permit(:name, :birth, :gpa, :grad)
    end
    
    • "White list" approach: specify which values you will allow the user to modify.
  • Handling post for creating new record:
    def create
      @student = Student.new(student_params(params[:student]))
      if @student.save then
        redirect_to(:action => :show)
      else
        render(:action => :edit)
      end
    end
    
  • Error handling in Rails forms:
    • Validation:
      class Student < ActiveRecord::Base
        validates_format_of :birth, :with => /\d\d\d\d-\d\d-\d\d/,
            :message => "must have format YYYY-MM-DD"
      
        def validate_gpa
          if (gpa < 0) || (gpa > 4.0) then
            errors.add(:gpa, "must be between 0.0 and 4.0")
          end
        end
      
        validate :validate_gpa
      end
      
      • Validation methods get invoked whenever the object is saved.
      • Several built-in validators, such as validates_format_of.
      • Can also define custom validators, such as validate_gpa.
      • errors.add saves an error message related to a particular attribute of the object.
      • If there is a validation error, methods such as save and update abort and return false.
    • After a validation error, the action method explicitly re-renders the form.
    • In the form view, extract and render all of the error messages for the object:
      <% @student.errors.full_messages.each do |msg| %>
        <p><%= msg %></p>
      <% end %>
      
    • Form helpers:
      • form.text_field and similar methods add an extra <div class="field_with_errors"> around any form elements and labels for which there are error messages: use CSS to display differently.

File uploads with Rails

  • If you use <input type="file">, then you must request a different protocol for posting data:
    <form method="POST" enctype="multipart/form-data">
    
  • Form helper to generate form element:
    form.file_field :photo
    
  • Rails will automatically add enctype="multipart/form-data" to the form element.
  • Rails knows how to handle this format (some frameworks don't):
    • Normal form data available in the normal way
    • Siphons off uploaded file to a file on disk; the params value for this form element is an object with lots of methods.
  • read method: returns contents of uploaded file.
  • original_filename method: returns file name as selected in the browser.