lib/express_templates/components/configurable.rb in express_templates-0.7.1 vs lib/express_templates/components/configurable.rb in express_templates-0.8.0
- old
+ new
@@ -1,40 +1,167 @@
module ExpressTemplates
module Components
+ # Configurable components support configuration options supplied to the
+ # builder method. Supported options must be declared. All other options
+ # are passed along and converted to html attributes.
+ #
+ # Example:
+ #
+ # ```ruby
+ #
+ # class Pane < ExpressTemplates::Components::Configurable
+ # has_option :title, "Displayed in the title area", required: true
+ # has_option :status, "Displayed in the status area"
+ # end
+ #
+ # # Usage:
+ #
+ # pane(title: "People", status: "#{people.count} people")
+ #
+ # ```ruby
+ #
+ # Options specified as required must be supplied.
+ #
+ # Default values may be supplied for options with a default: keyword.
+ #
+ # Options may be passed as html attributes with attribute: true
+ #
class Configurable < Base
- def self.emits(proc = nil, &block)
- define_method(:markup, &(proc || block))
+ class_attribute :supported_options
+ self.supported_options = {}
+
+ class_attribute :supported_arguments
+ self.supported_arguments = {}
+
+ def self.emits(*args, &block)
+ warn ".emits is deprecrated"
+ self.contains(*args, &block)
end
def build(*args, &block)
- _process_args!(args)
- if method(:markup).arity > 0
- markup(block)
- else
- markup(&block)
- end
+ _process_builder_args!(args)
+ super(*args, &block)
end
def config
@config ||= {}
end
- alias :my :config
+ def self.has_option(name, description, type: :text, required: nil, default: nil, attribute: nil)
+ raise "name must be a symbol" unless name.kind_of?(Symbol)
+ option_definition = {description: description}
+ option_definition.merge!(type: type, required: required, default: default, attribute: attribute)
+ self.supported_options =
+ self.supported_options.merge(name => option_definition)
+ end
+ def required_options
+ supported_options.select {|k,v| v[:required] unless v[:default] }
+ end
+ def self.has_argument(name, description, as: nil, type: :string, default: nil, optional: false)
+ raise "name must be a symbol" unless name.kind_of?(Symbol)
+ argument_definition = {description: description, as: as, type: type, default: default, optional: optional}
+ self.supported_arguments =
+ self.supported_arguments.merge(name => argument_definition)
+ end
+
+ has_argument :id, "The id of the component.", type: :symbol, optional: true
+
protected
- def _process_args!(args)
- if args.first.kind_of?(Symbol)
- config.merge!(id: args.shift)
- attributes[:id] = config[:id]
+ def _default_options
+ supported_options.select {|k,v| v[:default] }
+ end
+
+ def _check_required_options(supplied)
+ missing = required_options.keys - supplied.keys
+ if missing.any?
+ raise "#{self.class} missing required option(s): #{missing}"
end
- args.each do |arg|
- if arg.kind_of?(Hash)
- config.merge!(arg)
+ end
+
+ def _set_defaults
+ _default_options.each do |key, value|
+ if !!value[:attribute]
+ set_attribute key, value[:default]
+ else
+ config[key] ||= value[:default]
end
end
+ end
+
+ def _valid_types(definition)
+ valid_type_names = if definition[:type].kind_of?(Symbol)
+ [definition[:type]]
+ elsif definition[:type].respond_to?(:keys)
+ definition[:type].keys
+ else
+ definition[:type] || []
+ end
+ valid_type_names.map(&:to_s).map(&:classify).map(&:constantize)
+ end
+
+ def _is_valid?(value, definition)
+ valid_types = _valid_types(definition)
+ if valid_types.empty? && value.kind_of?(String)
+ true
+ elsif valid_types.include?(value.class)
+ true
+ else
+ false
+ end
+ end
+
+ def _optional_argument?(definition)
+ definition[:default] || definition[:optional]
+ end
+
+ def _required_argument?(definition)
+ !_optional_argument?(definition)
+ end
+
+ def _extract_supported_arguments!(args)
+ supported_arguments.each do |key, definition|
+ value = args.shift
+ if value.nil? && _required_argument?(definition)
+ raise "argument for #{key} not supplied"
+ end
+ unless _is_valid?(value, definition)
+ if _required_argument?(definition)
+ raise "argument for #{key} invalid (#{value.class}) '#{value.to_s}'; Allowable: #{_valid_types(definition).inspect}"
+ else
+ args.unshift value
+ next
+ end
+ end
+ config_key = definition[:as] || key
+ config[config_key] = value || definition[:default]
+ end
+ end
+
+ def _set_id_attribute
+ attributes[:id] = config[:id]
+ end
+
+ def _extract_supported_options!(builder_options)
+ builder_options.each do |key, value|
+ if supported_options.keys.include?(key)
+ unless supported_options[key][:attribute]
+ config[key] = builder_options.delete(key)
+ end
+ end
+ end
+ end
+
+ def _process_builder_args!(args)
+ _extract_supported_arguments!(args)
+ builder_options = args.last.try(:kind_of?, Hash) ? args.last : {}
+ _check_required_options(builder_options)
+ _extract_supported_options!(builder_options)
+ _set_defaults
+ _set_id_attribute
end
end
end
end
\ No newline at end of file