module Spark::Component::Element::ClassMethods

Public Instance Methods

define_element(name:, plural:, multiple:, klass:) click to toggle source

Element method will create a new element instance, or if no attributes or block is passed it will return the instance defined for that element.

For example when rendering a component, passing attributes or a block will create a new instance of that element.

# Some view (Slim)
= render(Nav) do |nav|
  - nav.item(href: "#url") { "link text" }

Then when referencing the element in the component's template it the method will return the instance. Call yield to output an elemnet's block

# Nav template (Slim)
nav
  - items.each do |item|
    a href=item.href
      = item.yield
# File lib/spark/component/element.rb, line 186
def define_element(name:, plural:, multiple:,  klass:)
  define_method_if_able(name) do |attributes = nil, &block|
    # When initializing an element, blocks or arguments are passed.
    # If an element is being referenced without these, it will return its instance
    # This allows the elemnet method to initailize the object and return its instance
    # for template rendering.
    unless block || attributes

      # If an element is called, it will only exist if its parent's block has been exectued
      # Be sure the block has been executed so that sub elements are initialized
      self.yield if is_a?(Spark::Component::Element::Base) && !rendered?

      return get_element_variable(multiple ? plural : name)
    end

    attributes ||= {}
    attributes = merge_element_attribute_default(name, attributes)
    attributes.merge!(_name: name, _parent: self, _block: block, _view: view_context)

    element = klass.new(attributes)

    # If element supports multiple instances, inject instance
    # into array for later enumeration
    if multiple
      get_element_variable(plural) << element
    else
      set_element_variable(name, element)
    end
  end

  return if !multiple || name == plural

  # Define a pluralized method name to access enumerable element instances.
  define_method_if_able(plural) do
    get_element_variable(plural)
  end
end
element(name, multiple: false, component: nil, &config) click to toggle source

Class method for adding elements

Options:

name: Symbol

  Create a method for interacting with an element
  This name cannot be the same as another instance method

multiple: Boolean (default: false)

  Defining `multiple: true` causes elements to be injected
  into an array. A pluralized method is created to access
  each element instance.

  For example, `element(:item, multiple: true)` will create
  an `:items` method and each time an item is executed, its
  instance will be added to items.

component: Class

  By default all elements include Element and extend its class methods
  Passing a class like `component: Nav::Item` will extend that component
  adding Element, Attributes, TagAttr and render methods.

&config: Block

  When defining a method, you may pass an optional block to
  configure attributes, nested elements, or even define methods.
# File lib/spark/component/element.rb, line 157
def element(name, multiple: false, component: nil, &config)
  plural_name = name.to_s.pluralize.to_sym if multiple
  klass = extend_class(component, &config)
  elements[name] = { multiple: plural_name, class: klass }

  define_element(name: name, plural: plural_name, multiple: multiple, klass: klass)
end
elements() click to toggle source
# File lib/spark/component/element.rb, line 123
def elements
  @elements ||= {}
end
inherited(child) click to toggle source
# File lib/spark/component/element.rb, line 114
def inherited(child)
  child.elements.replace(elements)
  child.attributes.replace(attributes)
  child.tag_attributes.replace(tag_attributes)
  child.data_attributes.replace(data_attributes)
  child.aria_attributes.replace(aria_attributes)
  child.attribute_default_groups.replace(attribute_default_groups)
end

Private Instance Methods

define_method_if_able(method_name, &block) click to toggle source

Prevent an element method from overwriting an existing method

# File lib/spark/component/element.rb, line 255
def define_method_if_able(method_name, &block)
  # Protect instance methods which are crucial to components and elements
  methods = [self, Element, Attribute]
  methods << Integration.base_class if defined? Spark::Component::Integration
  methods.map! { |c| c.instance_methods(false) }

  if methods.flatten.include?(method_name.to_sym)
    raise(Element::Error, "Method '#{method_name}' already exists.")
  end

  define_method(method_name, &block)
end
define_model_name(klass) click to toggle source

ActiveModel validations require a model_name. This connects new classes to their proper model names.

# File lib/spark/component/element.rb, line 246
def define_model_name(klass)
  klass.define_singleton_method(:model_name) do
    # try the current class, the parent class, or default to Spark::Component
    named_klass = [self.class, superclass, Spark::Component].reject { |k| k == Class }.first
    ActiveModel::Name.new(named_klass)
  end
end
extend_class(component, &config) click to toggle source

If an element extends a component, extend that class and include necessary modules

# File lib/spark/component/element.rb, line 227
def extend_class(component, &config)
  base = Class.new(component || Spark::Component::Element::Base, &config)
  define_model_name(base) if defined?(ActiveModel)

  return base unless component

  # Allow element to reference its source component
  base.define_singleton_method(:source_component) { component }

  # Override component when used as an element
  if defined?(Spark::Component::Integration)
    base.include(Spark::Component::Integration::Element)
  end

  base
end