Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Massive refactoring, completely new API

  • Loading branch information...
commit 8375d6b3bbdd2d616108c4231c75ec4bc22720f0 1 parent 1a4ac05
Jeremy Evans authored May 08, 2011
39  README.rdoc
Source Rendered
@@ -5,15 +5,42 @@ Forme is a HTML forms library for ruby with the following goals:
5 5
 1) Have no external dependencies
6 6
 2) Have a simple API
7 7
 3) Support forms both with and without related objects
8  
-4) Support both raw helpers and form builders
9  
-5) Allow compiling down to different types of output, by using
  8
+4) Allow compiling down to different types of output, by using
10 9
    an intermediate abstract syntax tree, similar to Sequel
11 10
 
12  
-= Other similar projects
  11
+= Basic Usage
13 12
 
14  
-1) Rails built-in helpers: requires Rails, violates goal 1
15  
-2) Formtastic: requires Rails, violates goal 1
16  
-3) padrino-helpers: requires ActiveSupport, violates goal 1
  13
+Without an object, is a simple form builder:
  14
+
  15
+  f = Forme::Form.new
  16
+  f.open(:action=>'/foo', :method=>:post) # '<form action="/foo" method="post">
  17
+  f.input(:textarea, :value=>'foo', :name=>'bar') # '<textarea name="bar">foo</textarea>'
  18
+  f.input(:text, :value=>'foo', :name=>'bar') # '<input name="bar" type="text" value="foo"/>'
  19
+  f.close # '</form>'
  20
+
  21
+With an object, calls +forme_input+ on the obj with the form, field, and options, which
  22
+should return a <tt>Forme::Input</tt> instance.
  23
+
  24
+  f = Forme::Form.new(obj)
  25
+  # obj.forme_input(f, :field, opts)
  26
+  # => Forme::Input.new(f, :text, :name=>'obj[field]', :id=>'obj_field', :value=>'foo')
  27
+  f.input(:field) # '<input id="obj_field" name="obj[field]" type="text" value="foo"/>'
  28
+
  29
+= Main Classes
  30
+
  31
+<tt>Forme::Form</tt> :: main object
  32
+<tt>Forme::Input</tt> :: high level abstract tag
  33
+<tt>Forme::Tag</tt> :: low level abstract tag 
  34
+<tt>Forme::Formatter</tt> :: takes input, returns tag
  35
+<tt>Forme::Serializer</tt> :: tags tag, returns string
  36
+
  37
+= Other Similar Projects
  38
+
  39
+All of these have external dependencies:
  40
+
  41
+1) Rails built-in helpers
  42
+2) Formtastic
  43
+3) padrino-helpers
17 44
 
18 45
 = Author
19 46
 
206  lib/forme.rb
@@ -2,110 +2,163 @@ module Forme
2 2
   class Error < StandardError
3 3
   end
4 4
 
5  
-  module Base
6  
-    WIDGETS = [:text, :password, :hidden, :checkbox, :radio, :submit, :textarea, :fieldset, :p, :div, :ol, :ul, :select, :optgroup, :legend, :li, :label, :option]
  5
+  class Form
  6
+    attr_reader :obj
  7
+    attr_reader :opts
  8
+    attr_reader :formatter
  9
+    attr_reader :serializer
  10
+    def initialize(obj=nil, opts={})
  11
+      @obj = obj
  12
+      @opts = opts
  13
+      @formatter = find_transformer(Formatter)
  14
+      @serializer = find_transformer(Serializer)
  15
+    end
7 16
 
8  
-    [:text, :password, :hidden, :checkbox, :radio, :submit].each do |x|
9  
-      class_eval("def #{x}(opts={}) Tag.new(:input, {:type=>:#{x}}.merge!(opts)) end", __FILE__, __LINE__)
  17
+    def input(field, opts={})
  18
+      if obj
  19
+        obj.forme_input(self, field, opts)
  20
+      else
  21
+        Input.new(self, field, opts)
  22
+      end.serialize
10 23
     end
11  
-    [:fieldset, :div, :ol, :ul, :select, :optgroup].each do |x|
12  
-      class_eval("def #{x}(opts={}, &block) Tag.new(:#{x}, opts, &block) end", __FILE__, __LINE__)
  24
+
  25
+    def open(attr)
  26
+      serializer.serialize_open(Tag.new(self, :form, attr))
13 27
     end
14  
-    [:textarea, :legend, :p, :li, :label, :option].each do |x|
15  
-      class_eval("def #{x}(text=nil, opts={}, &block) Tag.text_tag(:#{x}, text, opts, &block) end", __FILE__, __LINE__)
  28
+
  29
+    def close
  30
+      serializer.serialize_close(Tag.new(self, :form))
16 31
     end
17 32
 
18  
-    def option(text, value=nil, attr={}, opts={})
19  
-      attr = attr.merge(:value=>value) if value
20  
-      Tag.new(:option, attr, opts.merge(:text=>text))
  33
+    def tag(type, attr={})
  34
+      Tag.new(self, type, attr).serialize
  35
+    end
  36
+
  37
+    private
  38
+
  39
+    def find_transformer(klass)
  40
+      sym = klass.name.to_s.downcase.to_sym
  41
+      transformer ||= opts.fetch(sym, :default)
  42
+      transformer = klass.get_subclass_instance(transformer) if transformer.is_a?(Symbol)
  43
+      transformer
21 44
     end
22 45
   end
23  
-  include Base
24 46
 
25  
-  class Tag
  47
+  class Input
  48
+    attr_reader :form
26 49
     attr_reader :type
27 50
     attr_reader :opts
  51
+    def initialize(form, type, opts={})
  52
+      @form = form
  53
+      @type = type
  54
+      @opts = opts
  55
+    end
  56
+    def obj
  57
+      form.obj
  58
+    end
  59
+    def format
  60
+      form.formatter.format(self)
  61
+    end
  62
+    def serialize
  63
+      form.serializer.serialize(format)
  64
+    end
  65
+  end
  66
+
  67
+  class Tag
  68
+    attr_reader :form
  69
+    attr_reader :type
28 70
     attr_reader :attr
29 71
     attr_reader :children
30 72
 
31  
-    include Base
32  
-
33  
-    def self.text_tag(type, text, opts={}, &block)
34  
-      new(type, text.is_a?(Hash) ? text.merge(opts) : opts.merge(:text=>text), &block)
35  
-    end
36  
-
37  
-    def initialize(type, opts={}, &block)
  73
+    def initialize(form, type, attr={}, &block)
  74
+      @form = form
38 75
       @type = type
39  
-      @attr = opts[:attr] || {}
40  
-      @opts = opts
41  
-      [:type, :method, :class, :id, :cols, :rows, :action, :name, :value].each do |x|
42  
-        @attr[x] = opts[x] if opts[x]
43  
-      end
  76
+      @attr = attr
44 77
       @children = []
45  
-      self << opts[:text] if opts[:text]
46  
-      (block.arity == 1 ? yield(self) : instance_eval(&block)) if block
47 78
     end
48 79
 
49  
-    def html(formatter=nil)
50  
-      formatter ||= opts.fetch(:formatter, :default)
51  
-      if formatter.is_a?(Symbol)
52  
-        klass = Formatter::MAP[formatter]
53  
-        raise Error, "invalid formatter: #{formatter} (valid formatters: #{Formatter::MAP.keys.join(', ')})" unless klass
54  
-        formatter = klass.new
55  
-      end
56  
-      formatter.format(self)
  80
+    def <<(child)
  81
+      children << child
57 82
     end
58 83
 
59  
-    WIDGETS.each do |x|
60  
-      class_eval("def #{x}(*) add_tag(super) end", __FILE__, __LINE__)
61  
-    end
62  
-    
63  
-    def <<(s)
64  
-      raise Error, "self closing tags can't have children" if self_close?
65  
-      children << s
  84
+    def serialize
  85
+      form.serializer.serialize(self)
66 86
     end
  87
+  end
67 88
 
68  
-    def clone(opts={})
69  
-      t = super()
70  
-      t.instance_variable_set(:@opts,  self.opts.merge(opts))
71  
-      t
  89
+  module SubclassMap
  90
+    def self.extended(klass)
  91
+      klass.const_set(:MAP, {})
72 92
     end
73 93
 
74  
-    def input(fields, opts={})
75  
-      raise Error, "can only use #input if an :obj has been set" unless obj = self.opts[:obj]
76  
-      Array(fields).each{|f| add_tag(obj.send(:forme_tag, f, opts))}
  94
+    def get_subclass_instance(type)
  95
+      subclass = self::MAP[type] || self::MAP[:default]
  96
+      raise Error, "invalid #{name.to_s.downcase}: #{type} (valid #{name.to_s.downcase}s: #{klass::MAP.keys.join(', ')})" unless subclass 
  97
+      subclass.new
77 98
     end
78 99
 
79  
-    def self_close?
80  
-      [:input, :img].include?(type)
  100
+    def inherited(subclass)
  101
+      self::MAP[subclass.name.split('::').last.downcase.to_sym] = subclass
  102
+      super
81 103
     end
  104
+  end
82 105
 
83  
-    private
  106
+  class Formatter
  107
+    extend SubclassMap
  108
+  end
  109
+
  110
+  class Formatter::Default < Formatter
  111
+    def format(input)
  112
+      form = input.form
  113
+      attr = input.opts.dup
  114
+      tag = case t = input.type
  115
+      when :textarea, :fieldset, :div
  116
+        if val = attr.delete(:value)
  117
+          tg = Tag.new(form, t, attr)
  118
+          tg << val
  119
+          tg
  120
+        else
  121
+          tg = Tag.new(form, t, input.opts)
  122
+        end
  123
+      else
  124
+        Tag.new(form, :input, attr.merge!(:type=>t))
  125
+      end
  126
+
  127
+      if l = attr.delete(:label)
  128
+        label = Tag.new(form, :label)
  129
+        label << "#{l}: "
  130
+        label << tag
  131
+        tag = label
  132
+      end
84 133
 
85  
-    def add_tag(tag)
86  
-      children << tag
87 134
       tag
88 135
     end
89 136
   end
90 137
 
91  
-  class Tag::Formatter
92  
-    MAP = {}
93  
-    def self.inherited(subclass)
94  
-      MAP[subclass.name.split('::').last.downcase.to_sym] = subclass
95  
-      super
96  
-    end
  138
+  class Serializer
  139
+    extend SubclassMap
97 140
   end
98 141
 
99  
-  class Tag::Formatter::Default < Tag::Formatter
100  
-    def format(tag)
101  
-      sc = tag.self_close?
102  
-      if label = tag.opts[:label]
103  
-        format(Tag.new(:label, :text=>"#{label}: "){self << tag.clone(:label=>false)})
  142
+  class Serializer::Default < Serializer
  143
+    SELF_CLOSING = [:img, :input]
  144
+    def serialize(tag)
  145
+      if tag.is_a?(String)
  146
+        h tag
  147
+      elsif SELF_CLOSING.include?(tag.type)
  148
+        "<#{tag.type}#{attr_html(tag)}/>"
104 149
       else
105  
-        "<#{tag.type}#{attr_html(tag)}#{sc ? '/>' : ">"}#{children_html(tag)}#{"</#{tag.type}>" unless sc}"
  150
+        "#{serialize_open(tag)}#{children_html(tag)}#{serialize_close(tag)}"
106 151
       end
107 152
     end
108 153
 
  154
+    def serialize_open(tag)
  155
+      "<#{tag.type}#{attr_html(tag)}>"
  156
+    end
  157
+
  158
+    def serialize_close(tag)
  159
+      "</#{tag.type}>"
  160
+    end
  161
+
109 162
     private
110 163
 
111 164
     # Borrowed from Rack::Utils
@@ -127,26 +180,7 @@ def attr_html(tag)
127 180
     end
128 181
 
129 182
     def children_html(tag)
130  
-      tag.children.map{|x| x.respond_to?(:html) ? x.html(self) : (h x.to_s)}.join
131  
-    end
132  
-  end
133  
-
134  
-  class Tag::Formatter::Labels < Tag::Formatter
135  
-  end
136  
-
137  
-  def form(action_or_obj=nil, opts={}, &block)
138  
-    case action_or_obj
139  
-    when nil
140  
-      # nothing
141  
-    when String
142  
-      opts = {:action=>action_or_obj, :method=>:post}.merge!(opts)
143  
-    else
144  
-      opts = {:obj => action_or_obj}.merge!(opts)
  183
+      tag.children.map{|x| serialize(x)}.join
145 184
     end
146  
-    Tag.new(:form, opts, &block).html
147  
-  end
148  
-
149  
-  WIDGETS.each do |x|
150  
-    class_eval("def #{x}(*) super.html end", __FILE__, __LINE__)
151 185
   end
152 186
 end
97  spec/forme_simple_spec.rb
... ...
@@ -1,82 +1,63 @@
1 1
 require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb')
2 2
 
3  
-describe "Forme" do
4  
-  include Forme
  3
+describe "Forme plain forms" do
  4
+  before do
  5
+    @f = Forme::Form.new
  6
+  end
5 7
 
6 8
   specify "should create a simple input tags" do
7  
-    text.should == '<input type="text"/>'
8  
-    radio.should == '<input type="radio"/>'
  9
+    @f.input(:text).should == '<input type="text"/>'
  10
+    @f.input(:radio).should == '<input type="radio"/>'
9 11
   end
10 12
 
11 13
   specify "should create other tags" do
12  
-    textarea.should == '<textarea></textarea>'
13  
-    fieldset.should == '<fieldset></fieldset>'
14  
-  end
15  
-
16  
-  specify "should allow easy specifying of text as separate option for common tags containing text" do
17  
-    textarea('foo').should == '<textarea>foo</textarea>'
18  
-    p('bar').should == '<p>bar</p>'
  14
+    @f.input(:textarea).should == '<textarea></textarea>'
  15
+    @f.input(:fieldset).should == '<fieldset></fieldset>'
19 16
   end
20 17
 
21 18
   specify "should use html attributes specified in options" do
22  
-    text(:value=>'foo').should == '<input type="text" value="foo"/>'
23  
-    div(:class=>'bar').should == '<div class="bar"></div>'
  19
+    @f.input(:textarea, :value=>'foo', :name=>'bar').should == '<textarea name="bar">foo</textarea>'
  20
+    @f.input(:text, :value=>'foo', :name=>'bar').should == '<input name="bar" type="text" value="foo"/>'
24 21
   end
25 22
 
26  
-  specify "should support html attributes and text for text tags" do
27  
-    textarea('foo', :name=>'bar').should == '<textarea name="bar">foo</textarea>'
  23
+  specify "should automatically create a label if a :label option is used" do
  24
+    @f.input(:text, :label=>'Foo', :value=>'foo').should == '<label>Foo: <input type="text" value="foo"/></label>'
28 25
   end
29 26
 
30  
-  specify "should support just html attributes options for common tags containing text" do
31  
-    textarea(:name=>'bar').should == '<textarea name="bar"></textarea>'
  27
+  specify "#open should return an opening tag" do
  28
+    @f.open(:action=>'foo', :method=>'post').should == '<form action="foo" method="post">'
32 29
   end
33 30
 
34  
-  specify "#form should create an empty form" do
35  
-    form.should == '<form></form>'
  31
+  specify "#close should return a closing tag" do
  32
+    @f.close.should == '</form>'
36 33
   end
  34
+end
37 35
 
38  
-  specify "#form(action) should create a empty form with a post action" do
39  
-    form('foo').should == '<form action="foo" method="post"></form>'
40  
-  end
  36
+describe "Forme object forms" do
41 37
 
42  
-  specify "#form(action, :method=>:get) should create a empty form with a get action" do
43  
-    form('foo', :method=>:get).should == '<form action="foo" method="get"></form>'
  38
+  specify "should handle a simple case" do
  39
+    obj = Class.new{def forme_input(f, field, opts) Forme::Input.new(f, :text, :name=>"obj[#{field}]", :id=>"obj_#{field}", :value=>"#{field}_foo") end}.new 
  40
+    Forme::Form.new(obj).input(:field).should ==  '<input id="obj_field" name="obj[field]" type="text" value="field_foo"/>'
44 41
   end
45 42
 
46  
-  specify "#form should take a block and yield an argument to create tags" do
47  
-    form{|f| f.text}.should == '<form><input type="text"/></form>'
48  
-  end
49  
-
50  
-  specify "#form should take a block and instance eval it if the block accepts no arguments" do
51  
-    form{text}.should == '<form><input type="text"/></form>'
52  
-  end
53  
-
54  
-  specify "should be able to mix and match block argument and instance_eval modes" do
55  
-    form{|f| f.label('Text'){text}}.should == '<form><label>Text<input type="text"/></label></form>'
56  
-    form{label('Text'){|f| f.text}}.should == '<form><label>Text<input type="text"/></label></form>'
57  
-  end
58  
-
59  
-  specify "tags should accept a :label option to automatically create a label" do
60  
-    form{text(:label=>'Foo')}.should == '<form><label>Foo: <input type="text"/></label></form>'
61  
-  end
62  
-
63  
-  describe "object forms"
64  
-    before do
65  
-      @obj = Class.new do 
66  
-        def self.name() "Foo" end
67  
-
68  
-        attr_reader :x, :y
  43
+  specify "should handle more complex case with multiple different types and opts" do
  44
+    obj = Class.new do 
  45
+      def self.name() "Foo" end
69 46
 
70  
-        def initialize(x, y)
71  
-          @x, @y = x, y
72  
-        end
73  
-        def forme_tag(f, opts={})
74  
-          f == :x ? Forme::Tag.new(:textarea, {:label=>'X', :name=>'foo[x]', :id=>'foo_x', :text=>x}.merge!(opts)) : Forme::Tag.new(:input, {:label=>'Y', :name=>'foo[y]', :id=>'foo_y', :value=>y, :type=>:text}.merge!(opts))
75  
-        end
76  
-      end.new('&foo', 3)
77  
-    end
  47
+      attr_reader :x, :y
78 48
 
79  
-    specify "#form should accept a :obj option to set the object to use, with #input to create appropriate tags" do
80  
-      form(@obj){input [:x, :y]}.should == '<form><label>X: <textarea id="foo_x" name="foo[x]">&amp;foo</textarea></label><label>Y: <input id="foo_y" name="foo[y]" type="text" value="3"/></label></form>'
81  
-    end
  49
+      def initialize(x, y)
  50
+        @x, @y = x, y
  51
+      end
  52
+      def forme_input(form, field, opts={})
  53
+        t = opts[:type]
  54
+        t ||= (field == :x ? :textarea : :text)
  55
+        s = field.to_s
  56
+        Forme::Input.new(form, t, {:label=>s.upcase, :name=>"foo[#{s}]", :id=>"foo_#{s}", :value=>send(field)}.merge!(opts))
  57
+      end
  58
+    end.new('&foo', 3)
  59
+    f = Forme::Form.new(obj)
  60
+    f.input(:x).should == '<label>X: <textarea id="foo_x" name="foo[x]">&amp;foo</textarea></label>'
  61
+    f.input(:y, :brain=>'no').should == '<label>Y: <input brain="no" id="foo_y" name="foo[y]" type="text" value="3"/></label>'
82 62
   end
  63
+end

0 notes on commit 8375d6b

Please sign in to comment.
Something went wrong with that request. Please try again.