From 444fade6912ad0510e930e9d133046421cbaa02b Mon Sep 17 00:00:00 2001 From: Drew Bragg Date: Mon, 15 Aug 2022 16:44:04 -0400 Subject: [PATCH] feat: Add hash attributes functionality to view tag helpers (#589) * 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. --- .../lib/bridgetown-core/helpers.rb | 46 ++++++++++++++++--- bridgetown-core/test/test_ruby_helpers.rb | 14 ++++++ .../_docs/template-engines/erb-and-beyond.md | 29 ++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/bridgetown-core/lib/bridgetown-core/helpers.rb b/bridgetown-core/lib/bridgetown-core/helpers.rb index 7ded8191b..4b0b69961 100644 --- a/bridgetown-core/lib/bridgetown-core/helpers.rb +++ b/bridgetown-core/lib/bridgetown-core/helpers.rb @@ -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("#{text}") + 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}") + segments.join(" ") end # Forward all arguments to I18n.t method @@ -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 diff --git a/bridgetown-core/test/test_ruby_helpers.rb b/bridgetown-core/test/test_ruby_helpers.rb index 6bfaf12f0..fd217eaf5 100644 --- a/bridgetown-core/test/test_ruby_helpers.rb +++ b/bridgetown-core/test/test_ruby_helpers.rb @@ -32,6 +32,20 @@ def setup should "accept additional attributes" do assert_equal "Label", @helpers.link_to("Label", "/foo/bar", class: "classes", data_test: "abc123") end + + should "accept hash attributes" do + assert_equal "Label", @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 diff --git a/bridgetown-website/src/_docs/template-engines/erb-and-beyond.md b/bridgetown-website/src/_docs/template-engines/erb-and-beyond.md index 8a40135a5..4b562cf70 100644 --- a/bridgetown-website/src/_docs/template-engines/erb-and-beyond.md +++ b/bridgetown-website/src/_docs/template-engines/erb-and-beyond.md @@ -313,6 +313,17 @@ You can pass additional keyword arguments to `link_to` which will be translated Join our livestream! ``` +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" } %> + + +Join our livestream! +``` + +`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 @@ -328,6 +339,24 @@ Finally, if you pass a Ruby object (i.e., it responds to `url`), it will work as My last page ``` +## 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 +

>Hello, World!

+ + +

Hello, World!

+``` +`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 + + + + +``` + ## 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.