Permalink
Browse files

Add Rails integration

This is cleaner in some ways than the Sinatra integration, as
Rails templates hack ERB to allow <%= to be used with blocks,
and Rails already has code that allows you to switch output
buffers.  It still is fairly clunky in regards to what gets
emitted versus what gets returned, but it does pass the same
specs as the sinatra support.
  • Loading branch information...
1 parent 6a989c1 commit bd952f2c0ba8df92deda03046396d721d3196a0d @jeremyevans committed May 2, 2012
Showing with 272 additions and 10 deletions.
  1. +1 −0 .gitignore
  2. +2 −0 CHANGELOG
  3. +16 −10 README.rdoc
  4. +87 −0 lib/forme/rails.rb
  5. +166 −0 spec/rails_integration_spec.rb
View
1 .gitignore
@@ -2,3 +2,4 @@
/rdoc
/forme-*.gem
*.rbc
+/log
View
2 CHANGELOG
@@ -1,5 +1,7 @@
=== HEAD
+* Add Rails integration (jeremyevans)
+
* Make explicit labeler put label after checkboxes and radio buttons instead of before (jeremyevans)
* Make implicit labeler not include hidden checkbox inside label (jeremyevans)
View
26 README.rdoc
@@ -203,8 +203,8 @@ based on the format, numericality, and length validations.
= Sinatra Support
Forme ships with a Sinatra extension that you can get by <tt>require "forme/sinatra"</tt> and using
-<tt>helpers Forme::Sinatra::Helper</tt> in your Sinatra::Base subclass. It allows you to use the
-following API in your Sinatra Helper forms:
+<tt>helpers Forme::Sinatra::ERB</tt> in your Sinatra::Base subclass. It allows you to use the
+following API in your Sinatra ERB forms:
<% form(@obj, :action=>'/foo') do |f| %>
<%= f.input(:field) %>
@@ -213,17 +213,23 @@ following API in your Sinatra Helper forms:
<% end %>
<% end %>
-This example is for ERB/Erubis, but Forme has also been reported to work with haml and slim,
-and hopefully is generic enough to work with any template library Sinatra supports.
+This example is for ERB/Erubis. Other Sinatra template libraries work differently and
+probably do not support this integration.
-Note that if you use partials or anything that changes the @_out_buf instance variable,
-you have to reset the form's output in the partial before calling any method on the form,
-and also reset it after the partial returns:
+= Rails Support
- <%= f.output = @_out_buf %>
+Forme ships with a Rails extension that you can get by <tt>require "forme/rails"</tt> and using
+<tt>helpers Forme::Rails::ERB</tt> in your controller. If allows you to use the following API
+in your Rails forms:
-Hopefully, the integration can be made more complete in the future so that this is not
-required.
+ <%= forme(@obj, :action=>'/foo') do |f| %>
+ <%= f.input(:field) %>
+ <%= f.tag(:fieldset) do %>
+ <%= f.input(:field_two) %>
+ <% end %>
+ <% end %>
+
+This has been tested on Rails 3.2, but should hopefully work on Rails 3.0+.
= Other Similar Projects
View
87 lib/forme/rails.rb
@@ -0,0 +1,87 @@
+require 'forme'
+
+module Forme
+ module Rails # :nodoc:
+ # Subclass used when using Forme/Rails ERB integration,
+ # handling integration with the view template.
+ class Form < ::Forme::Form
+ # The Rails template that created this form.
+ attr_reader :template
+
+ # Set the template object when initializing.
+ def initialize(*)
+ super
+ @template = @opts[:template]
+ end
+
+ # Serialize and mark as already escaped the string version of
+ # the input.
+ def emit(tag)
+ template.output_buffer << template.raw(tag.to_s)
+ end
+
+ # Capture the inputs into a new output buffer, and return
+ # the buffer.
+ def inputs(*)
+ template.send(:with_output_buffer){super}
+ end
+
+ # If a block is not given, emit the inputs into the current output
+ # buffer.
+ def _inputs(*)
+ if block_given?
+ super
+ else
+ emit(super)
+ end
+ end
+
+ # Return a string version of the input that is already marked as safe.
+ def input(*)
+ template.raw(super.to_s)
+ end
+
+ # If a block is given, create a new output buffer and make sure all the
+ # output of the tag goes into that buffer, and return the buffer.
+ # Otherwise, just return a string version of the tag that is already
+ # marked as safe.
+ def tag(type, attr={}, children=[])
+ tag = _tag(type, attr, children)
+ if block_given?
+ template.send(:with_output_buffer) do
+ emit(serializer.serialize_open(tag)) if serializer.respond_to?(:serialize_open)
+ Array(children).each{|c| emit(c)}
+ yield self if block_given?
+ emit(serializer.serialize_close(tag)) if serializer.respond_to?(:serialize_close)
+ end
+ else
+ template.raw(tag.to_s)
+ end
+ end
+ end
+
+ module ERB
+ # Create a +Form+ object and yield it to the block,
+ # injecting the opening form tag before yielding and
+ # the closing form tag after yielding.
+ #
+ # Argument Handling:
+ # No args :: Creates a +Form+ object with no options and not associated
+ # to an +obj+, and with no attributes in the opening tag.
+ # 1 hash arg :: Treated as opening form tag attributes, creating a
+ # +Form+ object with no options.
+ # 1 non-hash arg :: Treated as the +Form+'s +obj+, with empty options
+ # and no attributes in the opening tag.
+ # 2 hash args :: First hash is opening attributes, second hash is +Form+
+ # options.
+ # 1 non-hash arg, 1-2 hash args :: First argument is +Form+'s obj, second is
+ # opening attributes, third if provided is
+ # +Form+'s options.
+ def forme(obj=nil, attr={}, opts={}, &block)
+ h = {:template=>self}
+ (obj.is_a?(Hash) ? attr = attr.merge(h) : opts = opts.merge(h))
+ Form.form(obj, attr, opts, &block)
+ end
+ end
+ end
+end
View
166 spec/rails_integration_spec.rb
@@ -0,0 +1,166 @@
+require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb')
+require File.join(File.dirname(File.expand_path(__FILE__)), 'sequel_helper.rb')
+
+require 'rubygems'
+require 'action_controller/railtie'
+require 'forme/rails'
+
+class FormeRails < Rails::Application
+ config.secret_token = routes.append { match ':action' , :controller=>'forme' }.inspect
+ config.active_support.deprecation = :stderr
+ config.middleware.delete(ActionDispatch::ShowExceptions)
+ config.threadsafe!
+ initialize!
+end
+
+class FormeController < ActionController::Base
+ helper Forme::Rails::ERB
+
+ def index
+ render :inline => <<END
+<%= forme([:foo, :bar], :action=>'/baz') do |f| %>
+ <p>FBB</p>
+ <%= f.input(:first) %>
+ <%= f.input(:last) %>
+<% end %>
+END
+ end
+
+ def nest
+ render :inline => <<END
+<%= forme([:foo, :bar], :action=>'/baz') do |f| %>
+ <%= f.tag(:p, {}, 'FBB') %>
+ <%= f.tag(:div) do %>
+ <%= f.input(:first) %>
+ <%= f.input(:last) %>
+ <% end %>
+
+<% end %>
+END
+ end
+
+ def nest_sep
+ @nest = <<END
+ n1
+ <%= f.tag(:div) do %>
+ n2
+ <%= f.input(:first) %>
+ <%= f.input(:last) %>
+ n3
+ <% end %>
+ n4
+ <%= f.inputs([:first, :last], :legend=>'Foo') %>
+ n5
+END
+ render :inline => <<END
+0
+<%= forme([:foo, :bar], :action=>'/baz') do |f| %>
+ 1
+ <%= f.tag(:p, {}, 'FBB') %>
+ 2
+ <%= render(:inline =>@nest, :locals=>{:f=>f}) %>
+ 3
+<% end %>
+4
+END
+ end
+
+ def nest_seq
+ @album = Album.load(:name=>'N', :copies_sold=>2, :id=>1)
+ @album.associations[:artist] = Artist.load(:name=>'A', :id=>2)
+ @nest = <<END
+ n1
+ <%= f.subform(:artist) do %>
+ n2
+ <%= f.input(:name2) %>
+ n3
+ <% end %>
+ n4
+ <%= f.subform(:artist, :inputs=>[:name3], :legend=>'Bar') %>
+ n5
+END
+ render :inline => <<END
+0
+<%= forme(@album, :action=>'/baz') do |f| %>
+ 1
+ <%= f.subform(:artist, :inputs=>[:name], :legend=>'Foo') %>
+ 2
+ <%= render(:inline=>@nest, :locals=>{:f=>f}) %>
+ 3
+<% end %>
+4
+END
+ end
+
+ def hash
+ render :inline => "<%= forme({:action=>'/baz'}, :obj=>[:foo]) do |f| %> <%= f.input(:first) %> <% end %>"
+ end
+
+ def legend
+ render :inline => <<END
+<%= forme([:foo, :bar], :action=>'/baz') do |f| %>
+ <p>FBB</p>
+ <%= f.inputs([:first, :last], :legend=>'Foo') %>
+ <p>FBB2</p>
+<% end %>
+END
+ end
+
+ def combined
+ render :inline => <<END
+<%= forme([:foo, :bar], {:action=>'/baz'}, :inputs=>[:first], :button=>'xyz', :legend=>'123') do |f| %>
+ <p>FBB</p>
+ <%= f.input(:last) %>
+<% end %>
+END
+ end
+
+ def noblock
+ render :inline => "<%= forme([:foo, :bar], {:action=>'/baz'}, :inputs=>[:first], :button=>'xyz', :legend=>'123') %>"
+ end
+end
+
+describe "Forme Rails integration" do
+ def sin_get(path)
+ res = FormeRails.call(@rack.merge('PATH_INFO'=>path))
+ p res unless res[0] == 200
+ res[2].join.gsub(/\s+/, ' ').strip
+ end
+ before do
+ o = Object.new
+ def o.puts(*) end
+ @rack = {'rack.input'=>'', 'REQUEST_METHOD'=>'GET', 'rack.errors'=>o}
+ end
+
+ specify "#form should add start and end tags and yield Forme::Form instance" do
+ sin_get('/index').should == '<form action="/baz"> <p>FBB</p> <input id="first" name="first" type="text" value="foo"/> <input id="last" name="last" type="text" value="bar"/> </form>'
+ end
+
+ specify "#form should add start and end tags and yield Forme::Form instance" do
+ sin_get('/nest').should == '<form action="/baz"> <p>FBB</p> <div> <input id="first" name="first" type="text" value="foo"/> <input id="last" name="last" type="text" value="bar"/> </div> </form>'
+ end
+
+ specify "#form should correctly handle situation where multiple templates are used with same form object" do
+ sin_get('/nest_sep').should == "0 <form action=\"/baz\"> 1 <p>FBB</p> 2 n1 <div> n2 <input id=\"first\" name=\"first\" type=\"text\" value=\"foo\"/> <input id=\"last\" name=\"last\" type=\"text\" value=\"bar\"/> n3 </div> n4 <fieldset class=\"inputs\"><legend>Foo</legend><input id=\"first\" name=\"first\" type=\"text\" value=\"foo\"/><input id=\"last\" name=\"last\" type=\"text\" value=\"bar\"/></fieldset> n5 3 </form>4"
+ end
+
+ specify "#form should correctly handle situation Sequel integration with subforms where multiple templates are used with same form object" do
+ sin_get('/nest_seq').should == "0 <form action=\"/baz\" class=\"forme album\" method=\"post\"> 1 <input id=\"album_artist_attributes_id\" name=\"album[artist_attributes][id]\" type=\"hidden\" value=\"2\"/><fieldset class=\"inputs\"><legend>Foo</legend><label>Name: <input id=\"album_artist_attributes_name\" name=\"album[artist_attributes][name]\" type=\"text\" value=\"A\"/></label></fieldset> 2 n1 <input id=\"album_artist_attributes_id\" name=\"album[artist_attributes][id]\" type=\"hidden\" value=\"2\"/> n2 <label>Name2: <input id=\"album_artist_attributes_name2\" name=\"album[artist_attributes][name2]\" type=\"text\" value=\"A2\"/></label> n3 n4 <input id=\"album_artist_attributes_id\" name=\"album[artist_attributes][id]\" type=\"hidden\" value=\"2\"/><fieldset class=\"inputs\"><legend>Bar</legend><label>Name3: <input id=\"album_artist_attributes_name3\" name=\"album[artist_attributes][name3]\" type=\"text\" value=\"A3\"/></label></fieldset> n5 3 </form>4"
+ end
+
+ specify "#form should accept two hashes instead of requiring obj as first argument" do
+ sin_get('/hash').should == '<form action="/baz"> <input id="first" name="first" type="text" value="foo"/> </form>'
+ end
+
+ specify "#form should deal with emitted code" do
+ sin_get('/legend').should == '<form action="/baz"> <p>FBB</p> <fieldset class="inputs"><legend>Foo</legend><input id="first" name="first" type="text" value="foo"/><input id="last" name="last" type="text" value="bar"/></fieldset> <p>FBB2</p> </form>'
+ end
+
+ specify "#form should work with :inputs, :button, and :legend options" do
+ sin_get('/combined').should == '<form action="/baz"><fieldset class="inputs"><legend>123</legend><input id="first" name="first" type="text" value="foo"/></fieldset> <p>FBB</p> <input id="last" name="last" type="text" value="bar"/> <input type="submit" value="xyz"/></form>'
+ end
+
+ specify "#form should work without a block" do
+ sin_get('/noblock').should == '<form action="/baz"><fieldset class="inputs"><legend>123</legend><input id="first" name="first" type="text" value="foo"/></fieldset><input type="submit" value="xyz"/></form>'
+ end
+end

0 comments on commit bd952f2

Please sign in to comment.