Skip to content

seanpdoyle/action_view-attributes

Repository files navigation

ActionView::Attributes

Transform Hash arguments into composable groups of HTML attributes

tag.attributes

Installing ActionView::Attributes::Helpers extends Action View's tag.attributes to return an instance of ActionView::Attributes.

ActionView::Attributes are Hash-like objects with two distinguishing abilities:

  1. They know how to serialize themselves into valid HTML attributes:

    attributes = tag.attributes(
      class: "border rounded-full p-4 aria-[expanded=false]:text-gray-500",
      aria: {
        controls: "a_disclosure",
        expanded: false
      },
      data: {
        controller: "disclosure",
        action: "click->disclosure#toggle",
        disclosure_element_outlet: "#a_disclosure"
      }
    )
    
    attributes.to_s
    # => class="border rounded-full p-4 aria-[expanded=false]:text-gray-500" aria-controls="a_disclosure" aria-expanded="false" data-controller="disclosure" data-action="click->disclosure#toggle" data-disclosure-element-outlet="#a_disclosure"
  2. They know how to deeply merge into attributes that are token lists:

    border = tag.attribute class: "border rounded-full"
    padding = tag.attribute class: "p-4"
    
    attributes = border.merge(padding).merge(class: "a-special-class")
    
    attributes.to_h
    # => { class: "border rounded-full p-4 a-special-class" }

Object#with_options

Since ActionView::Attributes instances are Hash-like, they're compatible with Object#with_options. Compose instances together to

def focusable
  tag.attributes class: "focus:outline-none focus:ring-2"
end

def button
  tag.attributes class: "py-2 px-4 font-semibold shadow-md"
end

def primary
  tag.attributes class: "bg-black rounded-lg text-white hover:bg-yellow-300 focus:ring-yellow-300 focus:ring-opacity-75"
end

def primary_button(...)
  tag.with_options([focusable, button, primary].reduce(:merge))
end

primary_button.button "Save", class: "uppercase"
#=> <button class="py-2 px-4 font-semibold shadow-md focus:outline-none focus:ring-2 bg-black rounded-lg text-white hover:bg-yellow-300 focus:ring-yellow-300 focus:ring-opacity-75 uppercase">
#=>   Save
#=> </button>

primary_button.a "Cancel", href: "/", class: "uppercase"
#=> <a href="/" class="py-2 px-4 font-semibold shadow-md focus:outline-none focus:ring-2 bg-black rounded-lg text-white hover:bg-yellow-300 focus:ring-yellow-300 focus:ring-opacity-75 uppercase">
#=>   Cancel
#=> </a>

Attribute support isn't limited to class:. Declare composable ARIA- and Stimulus-aware attributes with aria: and data: keys:

def disclosure(controls: nil, expanded: false)
  tag.attributes aria: {controls:, expanded:},
                 data: {controller: "disclosure", action: "click->disclosure#toggle", disclosure_element_outlet: ("#" + controls if controls)}
end

def primary_disclosure_button(controls: nil, expanded: false)
  tag.with_options([focusable, button, primary, disclosure(controls:, expanded:)].reduce(:merge))
end

primary_disclosure_button.button "Toggle", controls: "a_disclosure", expanded: true
#=> <button class="py-2 px-4 font-semibold shadow-md focus:outline-none focus:ring-2 bg-black rounded-lg text-white hover:bg-yellow-300 focus:ring-yellow-300 focus:ring-opacity-75"
#=>         aria-controls="a_disclosure" aria-expanded="true"
#=>         data-controller="disclosure" data-action="click->disclosure#toggle" data-disclosure-element-outlet="#a_disclosure">
#=>   Toggle
#=> </button>

primary_disclosure_button.summary "Toggle"
#=> <summary class="py-2 px-4 font-semibold shadow-md focus:outline-none focus:ring-2 bg-black rounded-lg text-white hover:bg-yellow-300 focus:ring-yellow-300 focus:ring-opacity-75"
#=>          data-controller="disclosure" data-action="click->disclosure#toggle">
#=>   Toggle
#=> </summary>

ActionView::Base#with_attributes

Inspired by #with_options, the #with_attributes is a short-hand method that combines any number of Hash-like arguments into an ActionView::Attributes instance, then passes that along as an argument to #with_options.

The #with_attributes method available both as an Action View helper method and as tag instance method.

with_attributes {class: "border rounded-full"}, {class: "p-4"}, class: "focus:outline-none focus:ring" do |styled|
  styled.link_to "A link", "/a-link"
  # => <a class="border rounded-full p-4 focus:outline-none focus:ring" href="/a-link">A link</a>

  styled.button_tag "A button", type: "button"
  # => <button class="border rounded-full p-4 focus:outline-none focus:ring" name="button" type="button">A button</button>
end

builder = tag.with_attributes {class: "border rounded-full"}, {class: "p-4"}, class: "focus:outline-none focus:ring"

builder.a "A link", href: "/a-link"
# => <a class="border rounded-full p-4 focus:outline-none focus:ring" href="/a-link">A link</a>

builder.button_tag "A button", type: "button"
# => <button class="border rounded-full p-4 focus:outline-none focus:ring" type="button">A button</button>

Installation

Add this line to your application's Gemfile:

gem "action_view-attributes", github: "seanpdoyle/action_view-attributes", tag: "v0.1.0"

And then execute:

$ bundle

Or install it yourself as:

$ gem install action_view-attributes

Contributing

Contribution directions go here.

License

The gem is available as open source under the terms of the MIT License.