Skip to content

Commit

Permalink
feat: Add hash attributes functionality to view tag helpers (#589)
Browse files Browse the repository at this point in the history
* feat: Add hash attributes functionality to view tag helpers

* test: Add tests for `attributes_from_options`

* docs: Update `link_to` docs

Add example for using hash as keyword value.

* docs: Reword and expand documentation.

- Reword `link_to` documentation re kwargs to html attrs.
- Add "Other HTML Helpers" section.
- Add `attributes_from_options` docs.
  • Loading branch information
DRBragg authored Aug 15, 2022
1 parent 22427a4 commit 444fade
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 6 deletions.
46 changes: 40 additions & 6 deletions bridgetown-core/lib/bridgetown-core/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,28 @@ def find_relative_url_for_path(relative_path)
# @return [String] the anchor tag HTML
# @raise [ArgumentError] if the file cannot be found
def link_to(text, relative_path, options = {})
segments = attributes_from_options({ href: url_for(relative_path) }.merge(options))

# TODO: this might leak an XSS string into text, need to check
safe("<a #{segments}>#{text}</a>")
end

# Create a set of attributes from a hash.
#
# @param options [Hash] key-value pairs of HTML attributes
# @return [String]
def attributes_from_options(options)
segments = []
segments << "a"
segments << "href=\"#{url_for(relative_path)}\""
options.each do |attr, option|
attr = attr.to_s.tr("_", "-")
segments << "#{attr}=\"#{Utils.xml_escape(option)}\""
attr = dashed(attr)
if option.is_a?(Hash)
option = option.transform_keys { |key| "#{attr}-#{dashed(key)}" }
segments << attributes_from_options(option)
else
segments << attribute_segment(attr, option)
end
end
# TODO: this might leak an XSS string into text, need to check
safe("<#{segments.join(" ")}>#{text}</a>")
segments.join(" ")
end

# Forward all arguments to I18n.t method
Expand All @@ -124,6 +137,27 @@ def safe(input)
input.to_s.html_safe
end
alias_method :raw, :safe

private

# Covert an underscored value into a dashed string.
#
# @example "foo_bar_baz" => "foo-bar-baz"
#
# @param value [String|Symbol]
# @return [String]
def dashed(value)
value.to_s.tr("_", "-")
end

# Create an attribute segment for a tag.
#
# @param attr [String] the HTML attribute name
# @param value [String] the attribute value
# @return [String]
def attribute_segment(attr, value)
"#{attr}=\"#{Utils.xml_escape(value)}\""
end
end
end
end
14 changes: 14 additions & 0 deletions bridgetown-core/test/test_ruby_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ def setup
should "accept additional attributes" do
assert_equal "<a href=\"/foo/bar\" class=\"classes\" data-test=\"abc123\">Label</a>", @helpers.link_to("Label", "/foo/bar", class: "classes", data_test: "abc123")
end

should "accept hash attributes" do
assert_equal "<a href=\"/foo/bar\" class=\"classes\" data-controller=\"test\" data-action=\"test#test\">Label</a>", @helpers.link_to("Label", "/foo/bar", class: "classes", data: { controller: "test", action: "test#test" })
end
end

context "attributes_from_options" do
should "return an attribute string from a hash" do
assert_equal "class=\"classes\" data-test=\"abc123\"", @helpers.attributes_from_options(class: "classes", data_test: "abc123")
end

should "handle nested hashes" do
assert_equal "class=\"classes\" data-controller=\"test\" data-action=\"test#test\" data-test-target=\"test_value\" data-test-index-value=\"1\"", @helpers.attributes_from_options(class: "classes", data: { controller: "test", action: "test#test", test: { target: "test_value", index_value: "1" } })
end
end

context "class_map" do
Expand Down
29 changes: 29 additions & 0 deletions bridgetown-website/src/_docs/template-engines/erb-and-beyond.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@ You can pass additional keyword arguments to `link_to` which will be translated
<a href="/events/livestream" class="event" data-expire="2020-11-08">Join our livestream!</a>
```

In order to simplify more complex lists of HTML attributes you may also pass a hash as the value of one of the keyword arguments. This will convert all pairs in the hash into HTML attributes and prepend each key in the hash with the keyword argument:

```eruby
<%%= link_to "Join our livestream!", "_events/livestream.md", data: { controller: "testable", action: "testable#test" } %>
<!-- output: -->
<a href="/events/livestream" data-controller="testable" data-action="testable#test">Join our livestream!</a>
```

`link_to` uses [`attributes_from_options`](#attributes_from_options) under the hood to handle this converstion.

You can also pass relative or aboslute URLs to `link_to` and they'll just pass-through to the anchor tag without change:

```eruby
Expand All @@ -328,6 +339,24 @@ Finally, if you pass a Ruby object (i.e., it responds to `url`), it will work as
<a href="/this/is/my-last-page">My last page</a>
```

## Other HTML Helpers

### attributes_from_options
`attributes_from_options` allows you to pass a hash and have it converted to a string of HTML attributes:
```eruby
<p <%= attributes_from_options({ class: "my-class", id: "some-id" }) %>>Hello, World!</p>
<!-- output: -->
<p class="my-class" id="some-id">Hello, World!</p>
```
`attributes_from_options` also allows for any value of the passed hash to itself be a hash. This will result in individual attributes being created from each pair in the hash. When doing this, the key the hash was paired with will be prepended to each attribute name:
```eruby
<button <%= attributes_from_options({ data: { controller: "clickable", action: "click->clickable#test" } }) %>>Click Me!</button>
<!-- output: -->
<button data-controller="clickable" data-action="click->clickable#test">Click Me!</button>
```

## Capture Helper

If you need to capture a part of your template and store it in a variable for later use, you can use the `capture` helper.
Expand Down

0 comments on commit 444fade

Please sign in to comment.