Permalink
Browse files

Massive refactoring, completely new API

  • Loading branch information...
1 parent 1a4ac05 commit 8375d6b3bbdd2d616108c4231c75ec4bc22720f0 @jeremyevans committed May 9, 2011
Showing with 192 additions and 150 deletions.
  1. +33 −6 README.rdoc
  2. +120 −86 lib/forme.rb
  3. +39 −58 spec/forme_simple_spec.rb
View
@@ -5,15 +5,42 @@ Forme is a HTML forms library for ruby with the following goals:
1) Have no external dependencies
2) Have a simple API
3) Support forms both with and without related objects
-4) Support both raw helpers and form builders
-5) Allow compiling down to different types of output, by using
+4) Allow compiling down to different types of output, by using
an intermediate abstract syntax tree, similar to Sequel
-= Other similar projects
+= Basic Usage
-1) Rails built-in helpers: requires Rails, violates goal 1
-2) Formtastic: requires Rails, violates goal 1
-3) padrino-helpers: requires ActiveSupport, violates goal 1
+Without an object, is a simple form builder:
+
+ f = Forme::Form.new
+ f.open(:action=>'/foo', :method=>:post) # '<form action="/foo" method="post">
+ f.input(:textarea, :value=>'foo', :name=>'bar') # '<textarea name="bar">foo</textarea>'
+ f.input(:text, :value=>'foo', :name=>'bar') # '<input name="bar" type="text" value="foo"/>'
+ f.close # '</form>'
+
+With an object, calls +forme_input+ on the obj with the form, field, and options, which
+should return a <tt>Forme::Input</tt> instance.
+
+ f = Forme::Form.new(obj)
+ # obj.forme_input(f, :field, opts)
+ # => Forme::Input.new(f, :text, :name=>'obj[field]', :id=>'obj_field', :value=>'foo')
+ f.input(:field) # '<input id="obj_field" name="obj[field]" type="text" value="foo"/>'
+
+= Main Classes
+
+<tt>Forme::Form</tt> :: main object
+<tt>Forme::Input</tt> :: high level abstract tag
+<tt>Forme::Tag</tt> :: low level abstract tag
+<tt>Forme::Formatter</tt> :: takes input, returns tag
+<tt>Forme::Serializer</tt> :: tags tag, returns string
+
+= Other Similar Projects
+
+All of these have external dependencies:
+
+1) Rails built-in helpers
+2) Formtastic
+3) padrino-helpers
= Author
View
@@ -2,110 +2,163 @@ module Forme
class Error < StandardError
end
- module Base
- WIDGETS = [:text, :password, :hidden, :checkbox, :radio, :submit, :textarea, :fieldset, :p, :div, :ol, :ul, :select, :optgroup, :legend, :li, :label, :option]
+ class Form
+ attr_reader :obj
+ attr_reader :opts
+ attr_reader :formatter
+ attr_reader :serializer
+ def initialize(obj=nil, opts={})
+ @obj = obj
+ @opts = opts
+ @formatter = find_transformer(Formatter)
+ @serializer = find_transformer(Serializer)
+ end
- [:text, :password, :hidden, :checkbox, :radio, :submit].each do |x|
- class_eval("def #{x}(opts={}) Tag.new(:input, {:type=>:#{x}}.merge!(opts)) end", __FILE__, __LINE__)
+ def input(field, opts={})
+ if obj
+ obj.forme_input(self, field, opts)
+ else
+ Input.new(self, field, opts)
+ end.serialize
end
- [:fieldset, :div, :ol, :ul, :select, :optgroup].each do |x|
- class_eval("def #{x}(opts={}, &block) Tag.new(:#{x}, opts, &block) end", __FILE__, __LINE__)
+
+ def open(attr)
+ serializer.serialize_open(Tag.new(self, :form, attr))
end
- [:textarea, :legend, :p, :li, :label, :option].each do |x|
- class_eval("def #{x}(text=nil, opts={}, &block) Tag.text_tag(:#{x}, text, opts, &block) end", __FILE__, __LINE__)
+
+ def close
+ serializer.serialize_close(Tag.new(self, :form))
end
- def option(text, value=nil, attr={}, opts={})
- attr = attr.merge(:value=>value) if value
- Tag.new(:option, attr, opts.merge(:text=>text))
+ def tag(type, attr={})
+ Tag.new(self, type, attr).serialize
+ end
+
+ private
+
+ def find_transformer(klass)
+ sym = klass.name.to_s.downcase.to_sym
+ transformer ||= opts.fetch(sym, :default)
+ transformer = klass.get_subclass_instance(transformer) if transformer.is_a?(Symbol)
+ transformer
end
end
- include Base
- class Tag
+ class Input
+ attr_reader :form
attr_reader :type
attr_reader :opts
+ def initialize(form, type, opts={})
+ @form = form
+ @type = type
+ @opts = opts
+ end
+ def obj
+ form.obj
+ end
+ def format
+ form.formatter.format(self)
+ end
+ def serialize
+ form.serializer.serialize(format)
+ end
+ end
+
+ class Tag
+ attr_reader :form
+ attr_reader :type
attr_reader :attr
attr_reader :children
- include Base
-
- def self.text_tag(type, text, opts={}, &block)
- new(type, text.is_a?(Hash) ? text.merge(opts) : opts.merge(:text=>text), &block)
- end
-
- def initialize(type, opts={}, &block)
+ def initialize(form, type, attr={}, &block)
+ @form = form
@type = type
- @attr = opts[:attr] || {}
- @opts = opts
- [:type, :method, :class, :id, :cols, :rows, :action, :name, :value].each do |x|
- @attr[x] = opts[x] if opts[x]
- end
+ @attr = attr
@children = []
- self << opts[:text] if opts[:text]
- (block.arity == 1 ? yield(self) : instance_eval(&block)) if block
end
- def html(formatter=nil)
- formatter ||= opts.fetch(:formatter, :default)
- if formatter.is_a?(Symbol)
- klass = Formatter::MAP[formatter]
- raise Error, "invalid formatter: #{formatter} (valid formatters: #{Formatter::MAP.keys.join(', ')})" unless klass
- formatter = klass.new
- end
- formatter.format(self)
+ def <<(child)
+ children << child
end
- WIDGETS.each do |x|
- class_eval("def #{x}(*) add_tag(super) end", __FILE__, __LINE__)
- end
-
- def <<(s)
- raise Error, "self closing tags can't have children" if self_close?
- children << s
+ def serialize
+ form.serializer.serialize(self)
end
+ end
- def clone(opts={})
- t = super()
- t.instance_variable_set(:@opts, self.opts.merge(opts))
- t
+ module SubclassMap
+ def self.extended(klass)
+ klass.const_set(:MAP, {})
end
- def input(fields, opts={})
- raise Error, "can only use #input if an :obj has been set" unless obj = self.opts[:obj]
- Array(fields).each{|f| add_tag(obj.send(:forme_tag, f, opts))}
+ def get_subclass_instance(type)
+ subclass = self::MAP[type] || self::MAP[:default]
+ raise Error, "invalid #{name.to_s.downcase}: #{type} (valid #{name.to_s.downcase}s: #{klass::MAP.keys.join(', ')})" unless subclass
+ subclass.new
end
- def self_close?
- [:input, :img].include?(type)
+ def inherited(subclass)
+ self::MAP[subclass.name.split('::').last.downcase.to_sym] = subclass
+ super
end
+ end
- private
+ class Formatter
+ extend SubclassMap
+ end
+
+ class Formatter::Default < Formatter
+ def format(input)
+ form = input.form
+ attr = input.opts.dup
+ tag = case t = input.type
+ when :textarea, :fieldset, :div
+ if val = attr.delete(:value)
+ tg = Tag.new(form, t, attr)
+ tg << val
+ tg
+ else
+ tg = Tag.new(form, t, input.opts)
+ end
+ else
+ Tag.new(form, :input, attr.merge!(:type=>t))
+ end
+
+ if l = attr.delete(:label)
+ label = Tag.new(form, :label)
+ label << "#{l}: "
+ label << tag
+ tag = label
+ end
- def add_tag(tag)
- children << tag
tag
end
end
- class Tag::Formatter
- MAP = {}
- def self.inherited(subclass)
- MAP[subclass.name.split('::').last.downcase.to_sym] = subclass
- super
- end
+ class Serializer
+ extend SubclassMap
end
- class Tag::Formatter::Default < Tag::Formatter
- def format(tag)
- sc = tag.self_close?
- if label = tag.opts[:label]
- format(Tag.new(:label, :text=>"#{label}: "){self << tag.clone(:label=>false)})
+ class Serializer::Default < Serializer
+ SELF_CLOSING = [:img, :input]
+ def serialize(tag)
+ if tag.is_a?(String)
+ h tag
+ elsif SELF_CLOSING.include?(tag.type)
+ "<#{tag.type}#{attr_html(tag)}/>"
else
- "<#{tag.type}#{attr_html(tag)}#{sc ? '/>' : ">"}#{children_html(tag)}#{"</#{tag.type}>" unless sc}"
+ "#{serialize_open(tag)}#{children_html(tag)}#{serialize_close(tag)}"
end
end
+ def serialize_open(tag)
+ "<#{tag.type}#{attr_html(tag)}>"
+ end
+
+ def serialize_close(tag)
+ "</#{tag.type}>"
+ end
+
private
# Borrowed from Rack::Utils
@@ -127,26 +180,7 @@ def attr_html(tag)
end
def children_html(tag)
- tag.children.map{|x| x.respond_to?(:html) ? x.html(self) : (h x.to_s)}.join
- end
- end
-
- class Tag::Formatter::Labels < Tag::Formatter
- end
-
- def form(action_or_obj=nil, opts={}, &block)
- case action_or_obj
- when nil
- # nothing
- when String
- opts = {:action=>action_or_obj, :method=>:post}.merge!(opts)
- else
- opts = {:obj => action_or_obj}.merge!(opts)
+ tag.children.map{|x| serialize(x)}.join
end
- Tag.new(:form, opts, &block).html
- end
-
- WIDGETS.each do |x|
- class_eval("def #{x}(*) super.html end", __FILE__, __LINE__)
end
end
Oops, something went wrong.

0 comments on commit 8375d6b

Please sign in to comment.