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 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_builder_args!(args) super(*args, &block) end def config @config ||= {} end 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 _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 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