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