Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form helper #16

Merged
merged 18 commits into from Jun 3, 2015
Merged

Form helper #16

merged 18 commits into from Jun 3, 2015

Conversation

jodosha
Copy link
Member

@jodosha jodosha commented Feb 22, 2015

Introduction

By including FormHelper it will inject one public method: form_for.
This is a HTML5 form generator.

Technical notes

Zero monkey-patch

This feature has a similar syntax to other Ruby gems with the same purpose, but it has a different usage if compared with Rails or Padrino.

Those frameworks allow a syntax like this:

<%= form_for :book do |f| %>
  <div>
    <%= f.text_field :title %>
  </div>
<% end %>

The code above isn't a valid ERB template. To make it work, Rails uses monkey-patches ERB, and Padrino supports only HAML with that syntax.

One of the pillars of Lotus is "zero monkey-patch of Ruby core and stdlib". We want to keep this principle for this feature too.

Template engine independent

Avoiding monkey-patch has an advantage here. Lotus::Helpers are designed to enhance Lotus::View. This framework supports a lot of template engines, because it's powered by Tilt.

This form generator is designed to be template engine independent.

One output block

The technical compromise for the principles described above is to use the form builder in an unique output block.

<%=
  form_for :book, routes.books_path do
    text_field :title

    submit 'Create'
  end
%>

This will produce

<form action="/books" id="book-form" method="POST">
  <input type="text" name="book[title]" id="book-id" value="">
  <button type="submit">Create</button>
</form>

Method in views

An alternative usage is to define a concrete method in a view and to use it in the template:

module Books
  class New
    include Lotus::View
    include Lotus::Helpers

    def form
      form_for :book, routes.books_path do
        text_field :title

        submit 'Create'
      end
    end
  end
end
<%= form %>

Features

  • Support for complex markup without the need of concatenation
  • Auto closing HTML5 tags
  • Support for view local variables
  • Method override support (PUT/PATCH/DELETE HTTP verbs aren't understood by browsers)
  • Automatic generation of HTML attributes for inputs: id, name, value.
  • Allow to override HTML attributes
  • Extract values from request params and fill value attributes
  • Automatic selection of current value for radio button and select inputs
  • Infinite nested fields

Supported tags and inputs

  • color_field
  • date_field
  • datetime_field
  • datetime_local_field
  • email_field
  • hidden_field
  • file_field
  • fields_for
  • form_for
  • label
  • text_field
  • password_field
  • radio_button
  • select
  • submit

Examples

Basic usage

<%=
  form_for :book, routes.books_path, class: 'form-horizontal' do
    div do
      label      :title
      text_field :title, class: 'form-control'
    end

    submit 'Create'
  end
%>
<form action="/books" id="book-form" method="POST" class="form-horizontal">
  <div>
    <label for="book-title">Title</label>
    <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
  </div>

  <button type="submit">Create</button>
</form>

Method override

<%=
  form_for :book, routes.book_path(book.id), method: :put do
    text_field :title

    submit 'Update'
  end
%>
<form action="/books/23" id="book-form" method="POST">
  <input type="hidden" name="_method" value="PUT">
  <input type="text" name="book[title]" id="book-title" value="Test Driven Development">

  <button type="submit">Update</button>
</form>

Nested fields

<%=
  form_for :delivery, routes.deliveries_path do
    text_field :customer_name

    fields_for :address do
      text_field :city
    end

    submit 'Create'
  end
%>
<form action="/deliveries" id="delivery-form" method="POST">
  <input type="text" name="delivery[customer_name]" id="delivery-customer-name" value="">
  <input type="text" name="delivery[address][city]" id="delivery-address-city" value="">

  <button type="submit">Create</button>
</form>

Form for resources

Form to create a new resource

<%=
  form_for :book, routes.books_path, class: 'form-horizontal' do
    text_field :title

    submit 'Create'
  end
%>
<form action="/books" id="book-form" method="POST" class="form-horizontal">
  <input type="text" name="book[title]" id="book-title" value="Test Driven Development">

  <button type="submit">Create</button>
</form>

Form to update an existing resource

<%=
  form_for :book, routes.book_path(id: book.id), values: { book: book }, class: 'form-horizontal' do
    text_field :title

    submit 'Update'
  end
%>
<form action="/books/23" id="book-form" method="POST" class="form-horizontal">
  <input type="hidden" name="_method" value="PUT">
  <input type="text" name="book[title]" id="book-title" value="Test Driven Development">

  <button type="submit">Update</button>
</form>

Share markup between new and update templates

module Books
  class New
    include Lotus::View
    include Lotus::Helpers

    def form
      Form.new(:book, routes.books_path)
    end

    def submit_label
      'Create'
    end
  end

  class Edit
    include Lotus::View
    include Lotus::Helpers

    def form
      Form.new(:book, routes.book_path(id: book.id)
        {book: book}, {method: :patch})
    end

    def submit_label
      'Update'
    end
  end
end
# book/new.html.erb
<%= render partial: 'deliveries/form' %>
# book/edit.html.erb
<%= render partial: 'deliveries/form' %>
# book/_form.html.erb
<%=
  form_for form, class: 'form-horizontal' do
    text_field :title

    submit submit_label
  end
%>

Automatic values

When a form needs to be rendered again because of validation failures, or when a form to update a resource is rendered, the form helper is able to fill the appropriate values.

The params will always take the precedence, then it will lookup for the values passed as first argument to form_for.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.43%) to 96.23% when pulling b580e46 on form-helper into c5cee43 on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+3.43%) to 96.23% when pulling b580e46 on form-helper into c5cee43 on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.56%) to 96.36% when pulling 00bd9f8 on form-helper into c5cee43 on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.6%) to 96.4% when pulling 2c96a18 on form-helper into c5cee43 on master.

@jodosha jodosha changed the title Introduced Lotus::Helpers::FormHelper Form helper Feb 23, 2015
@jodosha jodosha self-assigned this Feb 23, 2015
@coveralls
Copy link

Coverage Status

Coverage increased (+3.88%) to 96.68% when pulling 19df61b on form-helper into c5cee43 on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.88%) to 96.68% when pulling 74ff6a6 on form-helper into c5cee43 on master.

2 similar comments
@coveralls
Copy link

Coverage Status

Coverage increased (+3.88%) to 96.68% when pulling 74ff6a6 on form-helper into c5cee43 on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.88%) to 96.68% when pulling 74ff6a6 on form-helper into c5cee43 on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.85%) to 96.65% when pulling 3754bee on form-helper into c5cee43 on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+3.85%) to 96.65% when pulling 3754bee on form-helper into c5cee43 on master.

# This is a HTML5 form builder.
#
# To understand the general HTML5 builder syntax of this framework, please
# consider to have a look at <tt>Lotus::Helpers::HtmlHelper</tt> documentation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change 'please consider to have a look' to 'please consider having a look'

@tomkadwill
Copy link
Contributor

👍 Looks great. Love the idea of a form_for tag with no monkey patching.

@AlfonsoUceda
Copy link
Contributor

❤️

@thecatwasnot
Copy link

👍 agreed, love the no monkey patching

@jodosha jodosha modified the milestone: v0.2.0 Mar 24, 2015
@danelowe
Copy link

👍 I do love the no monkey patching. In my case, I really needed the ability to use HAML within my form (the form fields are inside much larger table). I made my own very quick and dirty helper to get it working for now with HAML.

Is it possible that a similar approach to Padrino could eventually be used for form_for?
#5 (comment)
rtomayko/tilt#68 (comment)

module Client end
module Client::Helpers
  module YieldableForm

    class Builder < Lotus::Helpers::FormHelper::FormBuilder

      CONTENT_TAGS.each do |tag|
        class_eval %{
          def #{ tag }(content = nil, attributes = nil, &blk)
            self.class.html_node.new(:#{ tag }, blk || content, attributes || content, options)
          end
        }
      end

      EMPTY_TAGS.each do |tag|
        class_eval %{
          def #{ tag }(attributes = nil)
            Lotus::Helpers::HtmlHelper::EmptyHtmlNode.new(:#{ tag }, attributes)
          end
        }
      end

      def with_content(content)
        form(content, @attributes)
      end
    end

    def yieldable_form_for(name, url, options = {}, &blk)
      init_haml_helpers
      values     = Lotus::Helpers::FormHelper::Values.new(options.delete(:values), params)
      verb       = :patch if values.update?
      attributes = {
          action: url, id: "#{ name }-form",
          method: verb || Lotus::Helpers::FormHelper::DEFAULT_METHOD
      }.merge(options)
      form = Builder.new(name, values, attributes)
      form.with_content(Lotus::Utils::Escape::SafeString.new(capture_haml(form, &blk)))
    end
  end
end

@jodosha
Copy link
Member Author

jodosha commented Apr 21, 2015

@danelowe What's that init_haml_helpers. I guess this would be only specific for HAML, correct?
I have an idea to introduce as next thing in this PR. What if we'll be able to use the following syntax?

= f = form_for(book)
  .input
    = f.label :title
    = f.text_input :title
= f.close

The difference for a dev is minimal: we assign f instead of yield it and we need to remember to close the form. But then the body of the form is the same of the usage that we do with helpers that uses "capture tricks".

What do you think?

@danelowe
Copy link

@jodosha init_haml_helpers and capture_haml are both HAML specific. init_haml_helpers is required to make capture_haml work. It looks like the approach Padrino took was to implement handlers for each template engine.

I like your suggestion. I had considered that approach. The only reason I didn't implement it was that I stopped as soon as I got something to at least work, and at the time wasn't certain if or how I could achieve your approach.

Thanks!

@jodosha
Copy link
Member Author

jodosha commented Apr 21, 2015

It looks like the approach Padrino took was to implement handlers for each template engine.

I want to avoid this.

I like your suggestion. I had considered that approach. The only reason I didn't implement it was that I stopped as soon as I got something to at least work

That's totally understandable 😸

@runlevel5
Copy link
Member

@jodosha overall, it looks 👍 , btw I am wondering if we should support the data attributes:

form_for blah, blah, :data => { :hello_world => '123' }

@jodosha
Copy link
Member Author

jodosha commented May 29, 2015

@joneslee85 The idea here is to be agnostic. We accept a Hash that represent key/values and they are rendered as HTML attributes.

text_field :title, 'data-hello-world': '123'

Three motivations behind this choice:

  • Simplicity: all the attributes are passed as they are. No exceptions to learn.
  • Performance: if we introduce a convention for :data we need a nested loop: the outer for general attrs and if the current is attr is :data, (or "data") loop over its keys.
  • Open for extension: HTML is a moving target. This design is general purposed, if new special attrs come out, we don't need to change anything.

More about the last point. HTML and Form helpers here are primary designed to understand hierarchical document concepts. We have concrete methods to build already known tags (eg. body), but we already have in place tag and empty_tag, to make this ready for new tags.

tag(:foo, 'Hi', id: 'bar') # => <foo id="bar">Hi</foo>
empty_tag(:foo, id: 'bar') # => <foo id="bar">

@runlevel5
Copy link
Member

@jodosha if so, i am good 👍 with that

@jodosha jodosha merged commit d29451b into master Jun 3, 2015
@jodosha jodosha mentioned this pull request Jun 3, 2015
@jodosha jodosha deleted the form-helper branch July 31, 2017 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants