Skip to content

Commit

Permalink
Merge pull request #48100 from jpbalarini/add-picture-tag-helper
Browse files Browse the repository at this point in the history
Add a picture_tag helper
  • Loading branch information
guilleiguaran committed May 12, 2023
2 parents d954155 + fac9218 commit a3adc6c
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 0 deletions.
48 changes: 48 additions & 0 deletions actionview/CHANGELOG.md
@@ -1,3 +1,51 @@
* Add support for the HTML picture tag. It supports passing a String, an Array or a Block.
Supports passing properties directly to the img tag via the `:image` key.
Since the picture tag requires an img tag, the last element you provide will be used for the img tag.
For complete control over the picture tag, a block can be passed, which will populate the contents of the tag accordingly.

Can be used like this for a single source:
```erb
<%= picture_tag("picture.webp") %>
```
which will generate the following:
```html
<picture>
<img src="/images/picture.webp" />
</picture>
```

For multiple sources:
```erb
<%= picture_tag("picture.webp", "picture.png", :class => "mt-2", :image => { alt: "Image", class: "responsive-img" }) %>
```
will generate:
```html
<picture class="mt-2">
<source srcset="/images/picture.webp" />
<source srcset="/images/picture.png" />
<img alt="Image" class="responsive-img" src="/images/picture.png" />
</picture>
```

Full control via a block:
```erb
<%= picture_tag(:class => "my-class") do %>
<%= tag(:source, :srcset => image_path("picture.webp")) %>
<%= tag(:source, :srcset => image_path("picture.png")) %>
<%= image_tag("picture.png", :alt => "Image") %>
<% end %>
```
will generate:
```html
<picture class="my-class">
<source srcset="/images/picture.webp" />
<source srcset="/images/picture.png" />
<img alt="Image" src="/images/picture.png" />
</picture>
```

*Juan Pablo Balarini*

* Remove deprecated support to passing instance variables as locals to partials.

*Rafael Mendonça França*
Expand Down
57 changes: 57 additions & 0 deletions actionview/lib/action_view/helpers/asset_tag_helper.rb
Expand Up @@ -435,6 +435,63 @@ def image_tag(source, options = {})
tag("img", options)
end

# Returns an HTML picture tag for the +sources+. If +sources+ is a string,
# a single picture tag will be returned. If +sources+ is an array, a picture
# tag with nested source tags for each source will be returned. The
# +sources+ can be full paths, files that exist in your public images
# directory, or Active Storage attachments. Since the picture tag requires
# an img tag, the last element you provide will be used for the img tag.
# For complete control over the picture tag, a block can be passed, which
# will populate the contents of the tag accordingly.
#
# ==== Options
#
# When the last parameter is a hash you can add HTML attributes using that
# parameter. Apart from all the HTML supported options, the following are supported:
#
# * <tt>:image</tt> - Hash of options that are passed directly to the +image_tag+ helper.
#
# ==== Examples
#
# picture_tag("picture.webp")
# # => <picture><img src="/images/picture.webp" /></picture>
# picture_tag("gold.png", :image => { :size => "20" }
# # => <picture><img height="20" src="/images/gold.png" width="20" /></picture>
# picture_tag("gold.png", :image => { :size => "45x70" })
# # => <picture><img height="70" src="/images/gold.png" width="45" /></picture>
# picture_tag("picture.webp", "picture.png")
# # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img src="/images/picture.png" /></picture>
# picture_tag("picture.webp", "picture.png", :image => { alt: "Image" })
# # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
# picture_tag(["picture.webp", "picture.png"], :image => { alt: "Image" })
# # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
# picture_tag(:class => "my-class") { tag(:source, :srcset => image_path("picture.webp")) + image_tag("picture.png", :alt => "Image") }
# # => <picture class="my-class"><source srcset="/images/picture.webp" /><img alt="Image" src="/images/picture.png" /></picture>
# picture_tag { tag(:source, :srcset => image_path("picture-small.webp"), :media => "(min-width: 600px)") + tag(:source, :srcset => image_path("picture-big.webp")) + image_tag("picture.png", :alt => "Image") }
# # => <picture><source srcset="/images/picture-small.webp" media="(min-width: 600px)" /><source srcset="/images/picture-big.webp" /><img alt="Image" src="/images/picture.png" /></picture>
#
# Active Storage blobs (images that are uploaded by the users of your app):
#
# picture_tag(user.profile_picture)
# # => <picture><img src="/rails/active_storage/blobs/.../profile_picture.webp" /></picture>
def picture_tag(*sources, &block)
sources.flatten!
options = sources.extract_options!.symbolize_keys
picture_options = options.except(:image)
image_options = options.fetch(:image, {})
skip_pipeline = options.delete(:skip_pipeline)
source_tags = []

content_tag(:picture, picture_options) do
if block.present?
capture(&block).html_safe
else
source_tags = sources.map { |source| tag("source", srcset: resolve_asset_source("image", source, skip_pipeline)) } if sources.size > 1
safe_join(source_tags << image_tag(sources.last, image_options))
end
end
end

# Returns an HTML video tag for the +sources+. If +sources+ is a string,
# a single video tag will be returned. If +sources+ is an array, a video
# tag with nested source tags for each source will be returned. The
Expand Down
82 changes: 82 additions & 0 deletions actionview/test/template/asset_tag_helper_test.rb
Expand Up @@ -247,6 +247,68 @@ def content_security_policy_nonce
%(image_tag("rss.gif", srcset: [["pic_640.jpg", "640w"], ["pic_1024.jpg", "1024w"]])) => %(<img srcset="/images/pic_640.jpg 640w, /images/pic_1024.jpg 1024w" src="/images/rss.gif" />)
}

PicturePathToTag = {
%(image_path("xml")) => %(/images/xml),
%(image_path("xml.webp")) => %(/images/xml.webp),
%(image_path("dir/xml.webp")) => %(/images/dir/xml.webp),
%(image_path("/dir/xml.webp")) => %(/dir/xml.webp)
}

PathToPictureToTag = {
%(path_to_image("xml")) => %(/images/xml),
%(path_to_image("xml.webp")) => %(/images/xml.webp),
%(path_to_image("dir/xml.webp")) => %(/images/dir/xml.webp),
%(path_to_image("/dir/xml.webp")) => %(/dir/xml.webp)
}

PictureUrlToTag = {
%(image_url("xml")) => %(http://www.example.com/images/xml),
%(image_url("xml.webp")) => %(http://www.example.com/images/xml.webp),
%(image_url("dir/xml.webp")) => %(http://www.example.com/images/dir/xml.webp),
%(image_url("/dir/xml.webp")) => %(http://www.example.com/dir/xml.webp)
}

UrlToPictureToTag = {
%(url_to_image("xml")) => %(http://www.example.com/images/xml),
%(url_to_image("xml.webp")) => %(http://www.example.com/images/xml.webp),
%(url_to_image("dir/xml.webp")) => %(http://www.example.com/images/dir/xml.webp),
%(url_to_image("/dir/xml.webp")) => %(http://www.example.com/dir/xml.webp)
}

PictureLinkToTag = {
%(picture_tag("picture.webp")) => %(<picture><img src="/images/picture.webp" /></picture>),
%(picture_tag("gold.png", :image => { :size => "20" })) => %(<picture><img height="20" src="/images/gold.png" width="20" /></picture>),
%(picture_tag("gold.png", :image => { :size => 20 })) => %(<picture><img height="20" src="/images/gold.png" width="20" /></picture>),
%(picture_tag("silver.png", :image => { :size => "90.9" })) => %(<picture><img height="90.9" src="/images/silver.png" width="90.9" /></picture>),
%(picture_tag("silver.png", :image => { :size => 90.9 })) => %(<picture><img height="90.9" src="/images/silver.png" width="90.9" /></picture>),
%(picture_tag("gold.png", :image => { :size => "45x70" })) => %(<picture><img height="70" src="/images/gold.png" width="45" /></picture>),
%(picture_tag("gold.png", :image => { "size" => "45x70" })) => %(<picture><img height="70" src="/images/gold.png" width="45" /></picture>),
%(picture_tag("silver.png", :image => { :size => "67.12x74.09" })) => %(<picture><img height="74.09" src="/images/silver.png" width="67.12" /></picture>),
%(picture_tag("silver.png", :image => { "size" => "67.12x74.09" })) => %(<picture><img height="74.09" src="/images/silver.png" width="67.12" /></picture>),
%(picture_tag("bronze.png", :image => { :size => "10x15.7" })) => %(<picture><img height="15.7" src="/images/bronze.png" width="10" /></picture>),
%(picture_tag("bronze.png", :image => { "size" => "10x15.7" })) => %(<picture><img height="15.7" src="/images/bronze.png" width="10" /></picture>),
%(picture_tag("platinum.png", :image => { :size => "4.9x20" })) => %(<picture><img height="20" src="/images/platinum.png" width="4.9" /></picture>),
%(picture_tag("platinum.png", :image => { "size" => "4.9x20" })) => %(<picture><img height="20" src="/images/platinum.png" width="4.9" /></picture>),
%(picture_tag("error.png", :image => { "size" => "45 x 70" })) => %(<picture><img src="/images/error.png" /></picture>),
%(picture_tag("error.png", :image => { "size" => "1,024x768" })) => %(<picture><img src="/images/error.png" /></picture>),
%(picture_tag("error.png", :image => { "size" => "768x1,024" })) => %(<picture><img src="/images/error.png" /></picture>),
%(picture_tag("error.png", :image => { "size" => "x" })) => %(<picture><img src="/images/error.png" /></picture>),
%(picture_tag("google.com.png")) => %(<picture><img src="/images/google.com.png" /></picture>),
%(picture_tag("slash..png")) => %(<picture><img src="/images/slash..png" /></picture>),
%(picture_tag(".pdf.png")) => %(<picture><img src="/images/.pdf.png" /></picture>),
%(picture_tag("http://www.rubyonrails.com/images/rails.png")) => %(<picture><img src="http://www.rubyonrails.com/images/rails.png" /></picture>),
%(picture_tag("//www.rubyonrails.com/images/rails.png")) => %(<picture><img src="//www.rubyonrails.com/images/rails.png" /></picture>),
%(picture_tag("mouse.png", :image => { :alt => nil })) => %(<picture><img src="/images/mouse.png" /></picture>),
%(picture_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :image => { :alt => nil })) => %(<picture><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /></picture>),
%(picture_tag("")) => %(<picture><img src="" /></picture>),
%(picture_tag("picture.webp", "picture.png")) => %(<picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img src="/images/picture.png" /></picture>),
%(picture_tag("picture.webp", "picture.png", :class => "my-class")) => %(<picture class="my-class"><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img src="/images/picture.png" /></picture>),
%(picture_tag("picture.webp", "picture.png", :image => { alt: "Image" })) => %(<picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>),
%(picture_tag(["picture.webp", "picture.png"], :image => { alt: "Image" })) => %(<picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>),
%(picture_tag(:class => "my-class") { tag(:source, :srcset => image_path("picture.webp")) + image_tag("picture.png", :alt => "Image") }) => %(<picture class="my-class"><source srcset="/images/picture.webp" /><img alt="Image" src="/images/picture.png" /></picture>),
%(picture_tag { tag(:source, :srcset => image_path("picture-small.webp"), :media => "(min-width: 600px)") + tag(:source, :srcset => image_path("picture-big.webp")) + image_tag("picture.png", :alt => "Image") }) => %(<picture><source srcset="/images/picture-small.webp" media="(min-width: 600px)" /><source srcset="/images/picture-big.webp" /><img alt="Image" src="/images/picture.png" /></picture>),
}

FaviconLinkToTag = {
%(favicon_link_tag) => %(<link href="/images/favicon.ico" rel="icon" type="image/x-icon" />),
%(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="icon" type="image/x-icon" />),
Expand Down Expand Up @@ -711,6 +773,26 @@ def test_preload_link_tag
PreloadLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_picture_path
PicturePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_path_to_picture_alias_for_picture_path
PathToPictureToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_picture_url
PictureUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_url_to_picture_alias_for_picture_url
UrlToPictureToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_picture_tag
PictureLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

def test_video_path
VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
Expand Down

0 comments on commit a3adc6c

Please sign in to comment.