Skip to content

Commit

Permalink
feat: add Hash support for class and style attributes (#737)
Browse files Browse the repository at this point in the history
### Description 📖

This pull request adds support for passing `Hash` in `class` and `style`
attributes in a way that feels idiomatic in Ruby and achieves
composability.

### Background 📜 

When writing Phlex components, things usually start simple:

```ruby
div(id: "hero", class: "section")
```

but when you need a conditional class, things get a bit awkward:

```ruby
div(id: "hero", **classes("section", ("active" if active)))
```

This proposal is meant to provide an alternative which is easier to
understand, and easier to write:

```ruby
div(id: "hero", class: ["section", active:])
```

### Examples 🧪

#### `class`

A component such as:

```ruby
def active = true

def inactive = false

def view_template
  div(class: ["dropdown", inactive:, active:])
end
```

would render:

```erb
<div class="dropdown active"></div>
```

#### `style`

A component such as:

```ruby
def view_template
  div(style: {font_size: "16px", opacity: 0})
end
```

would render:

```erb
<div style="font-size:16px;opacity:0;"></div>
```

### Precedents

This feels like the Ruby equivalent of what frameworks like
[Vue](https://vuejs.org/guide/essentials/class-and-style.html) provide
in their template system:

- [Class and Style
Bindings](https://vuejs.org/guide/essentials/class-and-style.html)

As noted by @Spone, this behavior is consistent with the
[`class_names`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-token_list)
helper in Rails.
  • Loading branch information
joeldrapper authored Jun 24, 2024
2 parents c688085 + bcd5099 commit 13c3a17
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 9 deletions.
107 changes: 98 additions & 9 deletions lib/phlex/sgml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -406,16 +406,32 @@ def __attributes__(attributes, buffer = +"")
when Integer, Float
buffer << " " << name << '="' << v.to_s << '"'
when Hash
__attributes__(
v.transform_keys { |subkey|
case subkey
when Symbol then"#{name}-#{subkey.name.tr('_', '-')}"
else "#{name}-#{subkey}"
end
}, buffer
)
case k
when :class
buffer << " " << name << '="' << __classes__(v).gsub('"', "&quot;") << '"'
when :style
buffer << " " << name << '="' << __styles__(v).gsub('"', "&quot;") << '"'
else
__attributes__(
v.transform_keys { |subkey|
case subkey
when Symbol then"#{name}-#{subkey.name.tr('_', '-')}"
else "#{name}-#{subkey}"
end
}, buffer
)
end
when Array
buffer << " " << name << '="' << v.compact.join(" ").gsub('"', "&quot;") << '"'
value = case k
when :class
__classes__(v)
when :style
__styles__(v)
else
v.compact.join(" ")
end

buffer << " " << name << '="' << value.gsub('"', "&quot;") << '"'
when Set
buffer << " " << name << '="' << v.to_a.compact.join(" ").gsub('"', "&quot;") << '"'
else
Expand All @@ -433,5 +449,78 @@ def __attributes__(attributes, buffer = +"")

buffer
end

# @api private
def __classes__(c)
case c
when String
c
when Symbol
c.name
when Array, Set
c.filter_map { |c| __classes__(c) }.join(" ")
when Hash
c.filter_map { |c, add|
next unless add
case c
when String then c
when Symbol then c.name.tr("_", "-").delete_suffix("?")
else raise ArgumentError, "Class keys should be Strings or Symbols."
end
}.join(" ")
when nil, false
nil
else
if c.respond_to?(:to_phlex_attribute_value)
c.to_phlex_attribute_value
elsif c.respond_to?(:to_str)
c.to_str
else
c.to_s
end
end
end

# @api private
def __styles__(s)
style = case s
when String
s
when Symbol
s.name
when Integer, Float
s.to_s
when Array, Set
s.filter_map { |s| __styles__(s) }.join
when Hash
buffer = +""
s.each do |k, v|
prop = case k
when String then k
when Symbol then k.name.tr("_", "-")
else raise ArgumentError, "Style keys should be Strings or Symbols."
end

value = __styles__(v)

if value
buffer << prop << ":" << value
end
end
buffer
when nil, false
return nil
else
if s.respond_to?(:to_phlex_attribute_value)
s.to_phlex_attribute_value
elsif s.respond_to?(:to_str)
s.to_str
else
s.to_s
end
end

style.end_with?(";") ? style : style + ";"
end
end
end
51 changes: 51 additions & 0 deletions quickdraw/sgml/attributes.test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

{div: "div"}.each do |method_name, tag|
describe "<#{tag}> with class array attribute" do
example = Class.new(Phlex::HTML) do
define_method :view_template do
send(method_name, class: ["class", nil, inactive: false, truthy: 1]) { "content" }
end
end

test "produces the correct output" do
expect(example.call) == %(<#{tag} class="class truthy">content</#{tag}>)
end
end

describe "<#{tag}> with class hash attribute" do
example = Class.new(Phlex::HTML) do
define_method :view_template do
send(method_name, class: {class: true, inactive: false, truthy: 1}) { "content" }
end
end

test "produces the correct output" do
expect(example.call) == %(<#{tag} class="class truthy">content</#{tag}>)
end
end

describe "<#{tag}> with style array attribute" do
example = Class.new(Phlex::HTML) do
define_method :view_template do
send(method_name, style: ["color: red", nil, font_weight: "bold", opacity: 0]) { "content" }
end
end

test "produces the correct output" do
expect(example.call) == %(<#{tag} style="color: red;font-weight:bold;opacity:0;">content</#{tag}>)
end
end

describe "<#{tag}> with style hash attribute" do
example = Class.new(Phlex::HTML) do
define_method :view_template do
send(method_name, style: {color: "red", word_break: nil, font_weight: "bold"}) { "content" }
end
end

test "produces the correct output" do
expect(example.call) == %(<#{tag} style="color:red;font-weight:bold;">content</#{tag}>)
end
end
end

0 comments on commit 13c3a17

Please sign in to comment.