Skip to content

Commit a3adc6c

Browse files
Merge pull request rails#48100 from jpbalarini/add-picture-tag-helper
Add a picture_tag helper
2 parents d954155 + fac9218 commit a3adc6c

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

actionview/CHANGELOG.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,51 @@
1+
* Add support for the HTML picture tag. It supports passing a String, an Array or a Block.
2+
Supports passing properties directly to the img tag via the `:image` key.
3+
Since the picture tag requires an img tag, the last element you provide will be used for the img tag.
4+
For complete control over the picture tag, a block can be passed, which will populate the contents of the tag accordingly.
5+
6+
Can be used like this for a single source:
7+
```erb
8+
<%= picture_tag("picture.webp") %>
9+
```
10+
which will generate the following:
11+
```html
12+
<picture>
13+
<img src="/images/picture.webp" />
14+
</picture>
15+
```
16+
17+
For multiple sources:
18+
```erb
19+
<%= picture_tag("picture.webp", "picture.png", :class => "mt-2", :image => { alt: "Image", class: "responsive-img" }) %>
20+
```
21+
will generate:
22+
```html
23+
<picture class="mt-2">
24+
<source srcset="/images/picture.webp" />
25+
<source srcset="/images/picture.png" />
26+
<img alt="Image" class="responsive-img" src="/images/picture.png" />
27+
</picture>
28+
```
29+
30+
Full control via a block:
31+
```erb
32+
<%= picture_tag(:class => "my-class") do %>
33+
<%= tag(:source, :srcset => image_path("picture.webp")) %>
34+
<%= tag(:source, :srcset => image_path("picture.png")) %>
35+
<%= image_tag("picture.png", :alt => "Image") %>
36+
<% end %>
37+
```
38+
will generate:
39+
```html
40+
<picture class="my-class">
41+
<source srcset="/images/picture.webp" />
42+
<source srcset="/images/picture.png" />
43+
<img alt="Image" src="/images/picture.png" />
44+
</picture>
45+
```
46+
47+
*Juan Pablo Balarini*
48+
149
* Remove deprecated support to passing instance variables as locals to partials.
250
351
*Rafael Mendonça França*

actionview/lib/action_view/helpers/asset_tag_helper.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,63 @@ def image_tag(source, options = {})
435435
tag("img", options)
436436
end
437437

438+
# Returns an HTML picture tag for the +sources+. If +sources+ is a string,
439+
# a single picture tag will be returned. If +sources+ is an array, a picture
440+
# tag with nested source tags for each source will be returned. The
441+
# +sources+ can be full paths, files that exist in your public images
442+
# directory, or Active Storage attachments. Since the picture tag requires
443+
# an img tag, the last element you provide will be used for the img tag.
444+
# For complete control over the picture tag, a block can be passed, which
445+
# will populate the contents of the tag accordingly.
446+
#
447+
# ==== Options
448+
#
449+
# When the last parameter is a hash you can add HTML attributes using that
450+
# parameter. Apart from all the HTML supported options, the following are supported:
451+
#
452+
# * <tt>:image</tt> - Hash of options that are passed directly to the +image_tag+ helper.
453+
#
454+
# ==== Examples
455+
#
456+
# picture_tag("picture.webp")
457+
# # => <picture><img src="/images/picture.webp" /></picture>
458+
# picture_tag("gold.png", :image => { :size => "20" }
459+
# # => <picture><img height="20" src="/images/gold.png" width="20" /></picture>
460+
# picture_tag("gold.png", :image => { :size => "45x70" })
461+
# # => <picture><img height="70" src="/images/gold.png" width="45" /></picture>
462+
# picture_tag("picture.webp", "picture.png")
463+
# # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img src="/images/picture.png" /></picture>
464+
# picture_tag("picture.webp", "picture.png", :image => { alt: "Image" })
465+
# # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
466+
# picture_tag(["picture.webp", "picture.png"], :image => { alt: "Image" })
467+
# # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
468+
# picture_tag(:class => "my-class") { tag(:source, :srcset => image_path("picture.webp")) + image_tag("picture.png", :alt => "Image") }
469+
# # => <picture class="my-class"><source srcset="/images/picture.webp" /><img alt="Image" src="/images/picture.png" /></picture>
470+
# 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") }
471+
# # => <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>
472+
#
473+
# Active Storage blobs (images that are uploaded by the users of your app):
474+
#
475+
# picture_tag(user.profile_picture)
476+
# # => <picture><img src="/rails/active_storage/blobs/.../profile_picture.webp" /></picture>
477+
def picture_tag(*sources, &block)
478+
sources.flatten!
479+
options = sources.extract_options!.symbolize_keys
480+
picture_options = options.except(:image)
481+
image_options = options.fetch(:image, {})
482+
skip_pipeline = options.delete(:skip_pipeline)
483+
source_tags = []
484+
485+
content_tag(:picture, picture_options) do
486+
if block.present?
487+
capture(&block).html_safe
488+
else
489+
source_tags = sources.map { |source| tag("source", srcset: resolve_asset_source("image", source, skip_pipeline)) } if sources.size > 1
490+
safe_join(source_tags << image_tag(sources.last, image_options))
491+
end
492+
end
493+
end
494+
438495
# Returns an HTML video tag for the +sources+. If +sources+ is a string,
439496
# a single video tag will be returned. If +sources+ is an array, a video
440497
# tag with nested source tags for each source will be returned. The

actionview/test/template/asset_tag_helper_test.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,68 @@ def content_security_policy_nonce
247247
%(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" />)
248248
}
249249

250+
PicturePathToTag = {
251+
%(image_path("xml")) => %(/images/xml),
252+
%(image_path("xml.webp")) => %(/images/xml.webp),
253+
%(image_path("dir/xml.webp")) => %(/images/dir/xml.webp),
254+
%(image_path("/dir/xml.webp")) => %(/dir/xml.webp)
255+
}
256+
257+
PathToPictureToTag = {
258+
%(path_to_image("xml")) => %(/images/xml),
259+
%(path_to_image("xml.webp")) => %(/images/xml.webp),
260+
%(path_to_image("dir/xml.webp")) => %(/images/dir/xml.webp),
261+
%(path_to_image("/dir/xml.webp")) => %(/dir/xml.webp)
262+
}
263+
264+
PictureUrlToTag = {
265+
%(image_url("xml")) => %(http://www.example.com/images/xml),
266+
%(image_url("xml.webp")) => %(http://www.example.com/images/xml.webp),
267+
%(image_url("dir/xml.webp")) => %(http://www.example.com/images/dir/xml.webp),
268+
%(image_url("/dir/xml.webp")) => %(http://www.example.com/dir/xml.webp)
269+
}
270+
271+
UrlToPictureToTag = {
272+
%(url_to_image("xml")) => %(http://www.example.com/images/xml),
273+
%(url_to_image("xml.webp")) => %(http://www.example.com/images/xml.webp),
274+
%(url_to_image("dir/xml.webp")) => %(http://www.example.com/images/dir/xml.webp),
275+
%(url_to_image("/dir/xml.webp")) => %(http://www.example.com/dir/xml.webp)
276+
}
277+
278+
PictureLinkToTag = {
279+
%(picture_tag("picture.webp")) => %(<picture><img src="/images/picture.webp" /></picture>),
280+
%(picture_tag("gold.png", :image => { :size => "20" })) => %(<picture><img height="20" src="/images/gold.png" width="20" /></picture>),
281+
%(picture_tag("gold.png", :image => { :size => 20 })) => %(<picture><img height="20" src="/images/gold.png" width="20" /></picture>),
282+
%(picture_tag("silver.png", :image => { :size => "90.9" })) => %(<picture><img height="90.9" src="/images/silver.png" width="90.9" /></picture>),
283+
%(picture_tag("silver.png", :image => { :size => 90.9 })) => %(<picture><img height="90.9" src="/images/silver.png" width="90.9" /></picture>),
284+
%(picture_tag("gold.png", :image => { :size => "45x70" })) => %(<picture><img height="70" src="/images/gold.png" width="45" /></picture>),
285+
%(picture_tag("gold.png", :image => { "size" => "45x70" })) => %(<picture><img height="70" src="/images/gold.png" width="45" /></picture>),
286+
%(picture_tag("silver.png", :image => { :size => "67.12x74.09" })) => %(<picture><img height="74.09" src="/images/silver.png" width="67.12" /></picture>),
287+
%(picture_tag("silver.png", :image => { "size" => "67.12x74.09" })) => %(<picture><img height="74.09" src="/images/silver.png" width="67.12" /></picture>),
288+
%(picture_tag("bronze.png", :image => { :size => "10x15.7" })) => %(<picture><img height="15.7" src="/images/bronze.png" width="10" /></picture>),
289+
%(picture_tag("bronze.png", :image => { "size" => "10x15.7" })) => %(<picture><img height="15.7" src="/images/bronze.png" width="10" /></picture>),
290+
%(picture_tag("platinum.png", :image => { :size => "4.9x20" })) => %(<picture><img height="20" src="/images/platinum.png" width="4.9" /></picture>),
291+
%(picture_tag("platinum.png", :image => { "size" => "4.9x20" })) => %(<picture><img height="20" src="/images/platinum.png" width="4.9" /></picture>),
292+
%(picture_tag("error.png", :image => { "size" => "45 x 70" })) => %(<picture><img src="/images/error.png" /></picture>),
293+
%(picture_tag("error.png", :image => { "size" => "1,024x768" })) => %(<picture><img src="/images/error.png" /></picture>),
294+
%(picture_tag("error.png", :image => { "size" => "768x1,024" })) => %(<picture><img src="/images/error.png" /></picture>),
295+
%(picture_tag("error.png", :image => { "size" => "x" })) => %(<picture><img src="/images/error.png" /></picture>),
296+
%(picture_tag("google.com.png")) => %(<picture><img src="/images/google.com.png" /></picture>),
297+
%(picture_tag("slash..png")) => %(<picture><img src="/images/slash..png" /></picture>),
298+
%(picture_tag(".pdf.png")) => %(<picture><img src="/images/.pdf.png" /></picture>),
299+
%(picture_tag("http://www.rubyonrails.com/images/rails.png")) => %(<picture><img src="http://www.rubyonrails.com/images/rails.png" /></picture>),
300+
%(picture_tag("//www.rubyonrails.com/images/rails.png")) => %(<picture><img src="//www.rubyonrails.com/images/rails.png" /></picture>),
301+
%(picture_tag("mouse.png", :image => { :alt => nil })) => %(<picture><img src="/images/mouse.png" /></picture>),
302+
%(picture_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :image => { :alt => nil })) => %(<picture><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /></picture>),
303+
%(picture_tag("")) => %(<picture><img src="" /></picture>),
304+
%(picture_tag("picture.webp", "picture.png")) => %(<picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img src="/images/picture.png" /></picture>),
305+
%(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>),
306+
%(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>),
307+
%(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>),
308+
%(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>),
309+
%(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>),
310+
}
311+
250312
FaviconLinkToTag = {
251313
%(favicon_link_tag) => %(<link href="/images/favicon.ico" rel="icon" type="image/x-icon" />),
252314
%(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="icon" type="image/x-icon" />),
@@ -711,6 +773,26 @@ def test_preload_link_tag
711773
PreloadLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
712774
end
713775

776+
def test_picture_path
777+
PicturePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
778+
end
779+
780+
def test_path_to_picture_alias_for_picture_path
781+
PathToPictureToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
782+
end
783+
784+
def test_picture_url
785+
PictureUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
786+
end
787+
788+
def test_url_to_picture_alias_for_picture_url
789+
UrlToPictureToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
790+
end
791+
792+
def test_picture_tag
793+
PictureLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
794+
end
795+
714796
def test_video_path
715797
VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
716798
end

0 commit comments

Comments
 (0)