require 'glue/builder/xml' require 'nitro/control' require 'nitro/control/none' require 'nitro/control/attribute/fixnum' require 'nitro/control/attribute/float' require 'nitro/control/attribute/text' require 'nitro/control/attribute/password' require 'nitro/control/attribute/textarea' require 'nitro/control/attribute/checkbox' require 'nitro/control/attribute/options' require 'nitro/control/attribute/file' require 'nitro/control/relation/refers_to' require 'nitro/control/relation/has_many' module Nitro module FormHelper # A specialized Builder for dynamically building of forms. # Provides extra support for forms backed by managed objects # (entities). #-- # TODO: allow multiple objects per form. # TODO: use more generalized controls. #++ class FormXmlBuilder < ::Glue::XmlBuilder # Mappings of control names to controls. setting :control_map, :doc => 'Mappings of control names to controls', :default => { :fixnum => FixnumControl, :integer => FixnumControl, :float => FloatControl, :true_class => CheckboxControl, :boolean => CheckboxControl, :checkbox => CheckboxControl, :string => TextControl, :password => PasswordControl, :textarea => TextareaControl, :file => FileControl, :webfile => FileControl, =begin :array => ArrayControl, =end :options => OptionsControl, :refers_to => RefersToControl, :has_one => RefersToControl, :belongs_to => RefersToControl, :has_many => HasManyControl, :many_to_many => HasManyControl, :joins_many => HasManyControl } # Returns a control for the given objects attribute. def self.control_for(obj, a, anno, options) name = anno[:control] || anno[:class].to_s.demodulize.underscore.to_sym control_class = self.control_map.fetch(name, NoneControl) return control_class.new(obj, a, options) end # Returns a control for the given objects relation. def self.control_for_relation(obj, rel, options) name = rel[:control] || rel.class.to_s.demodulize.underscore.to_sym control_class = self.control_map.fetch(name, NoneControl) return control_class.new(obj, rel, options) end def initialize(buffer = '', options = {}) super @obj = options[:object] @errors = options[:errors] end # Render a control+label for the given property of the form # object. def attribute(a, options = {}) if anno = @obj.class.ann[a] control = self.class.control_for(@obj, a, anno, options) print element(a, anno, control.render) else raise "Undefined attribute '#{a}' for class '#{@obj.class}'." end end alias attr attribute # Render controls for all attributes of the form object. # It only considers serializable attributes. def all_attributes(options = {}) for a in @obj.class.serializable_attributes prop = @obj.class.ann(a) unless options[:all] next if a == @obj.class.primary_key or prop[:control] == :none or prop[:relation] or [options[:exclude]].flatten.include?(a) end attribute a, options end end alias attributes all_attributes alias serializable_attributes all_attributes # === Input # # * rel = The relation name as symbol, or the actual # relation object. #-- # FIXME: Fix the mismatch with the attributes. #++ def relation(rel, options = {}) # If the relation name is passed, lookup the actual # relation. if rel.is_a? Symbol rel = @obj.class.relation(rel) end control = self.class.control_for_relation(@obj, rel, options) print element(rel[:symbol], rel, control.render) end alias rel relation # Render controls for all relations of the form object. def all_relations(options = {}) for rel in @obj.class.relations unless options[:all] # Ignore polymorphic_marker relations. #-- # gmosx: should revisit the handling of polymorphic # relations, feels hacky. #++ next if (rel[:control] == :none) or rel.polymorphic_marker? end relation rel, options end end alias relations all_relations # Renders a control to select a file for upload. def select_file(name, options = {}) print %|| end # If flash[:ERRORS] is filled with errors structured as # name/message pairs the method creates a div containing them, # otherwise it returns an empty string. # # So you can write code like # #{form_errors} #
# # and redirect the user to the form in case of errors, thus # allowing him to see what was wrong. def form_errors res = '' unless @errors.empty? res << %{#{html}
} end end private # A sophisticated form generation helper method. # If no block is provided, render all attributes. # # === Options # # * :object, :entity, :class = The object that acts as model # for this form. If you pass a class an empty object is # instantiated. # # * :action = The action of this form. The parameter is # passed through the R operator (encode_url) to support # advanced url encoding. # # * :errors = An optional collection of errors. # # === Example # # #{form(:object => @owner, :action => :save_profile) do |f| # f.property :name, :editable => false # f.property :password # f.br # f.submit 'Update' # end} def form(options = {}, &block) obj = (options[:object] ||= options[:entity] || options[:class]) # If the passed obj is a Class instantiate an empty object # of this class. if obj.is_a? Class obj = options[:object] = obj.allocate end # Convert virtual :multipart method to method="post", # enctype="multipart/form-data" if options[:method] == :multipart options[:method] = 'POST' options[:enctype] = 'multipart/form-data' end options[:errors] ||= [] if errors = flash[:ERRORS] if errors.is_a? Array options[:errors].concat(errors) elsif errors.is_a? Glue::Validation::Errors options[:errors].concat(errors.to_a) else options[:errors] << errors end end if obj and errors = obj.errors options[:errors].concat(errors.to_a) end b = FormXmlBuilder.new('', options) b << '' return b end end end