Summary
In RubyUI::Avatar, an uncached avatar image can get permanently stuck on the fallback and never appear. The avatar_controller hides the still-loading <img> via display:none on connect(), but the image is rendered with loading="lazy". Browsers do not load loading="lazy" images whose box is not generated (display:none), so the load event never fires and the controller never reveals the image.
- Gem version:
ruby_ui 1.3.0 - Affected files:
lib/ruby_ui/avatar/avatar_controller.js, lib/ruby_ui/avatar/avatar_image.rb
Relevant code
avatar_image.rb renders the image as lazy:
defdefault_attrs{loading: "lazy",data: {ruby_ui__avatar_target: "image",action: "load->ruby-ui--avatar#showImage error->ruby-ui--avatar#showFallback"},
...
}endavatar_controller.js hides any image that isn't already complete:
connect(){if(!this.hasImageTarget){return;}if(this.imageTarget.complete&&this.imageTarget.naturalWidth>0){this.showImage();}else{// Image not yet loaded (or failed): hide it so the fallback shows.// Image visibility is restored by the load/error handlers.this.showFallback();// <-- adds `hidden` (display:none) to the <img>}}Why it breaks
- On first (uncached) render, at
connect() the image is not yet complete, so showFallback() runs and adds hidden → display:none to the <img>. - Because the image is
loading="lazy"and now display:none, the browser defers/skips its load (per the lazy-loading spec, images that generate no box are not loaded until shown). - The
load event therefore never fires, so showImage() is never called and the image stays hidden indefinitely — only the fallback is visible.
This is a self-contradiction in the component: loading="lazy" asks the browser to defer loading until the element is visible, while connect() makes the element invisible before it has loaded.
Steps to reproduce
- Render an
Avatar with an AvatarImage pointing at an uncached URL (hard refresh / disable cache). - Observe: fallback shows, real image never appears. Network tab shows the image request is never made (or deferred forever).
- Cached images appear fine, because
imageTarget.complete is already true at connect().
Suggested fix
Don't hide a still-loading image. Only fall back when the image has definitively failed (already complete with zero natural size); leave loading/successful images visible and let the load/error actions handle later transitions:
connect(){if(!this.hasImageTarget){return;}// Only act on an image that has already *failed*. A still-loading image must// stay visible — hiding it (display:none) would also stop a loading="lazy"// image from ever loading. Late load/error events are handled by the actions.if(this.imageTarget.complete&&this.imageTarget.naturalWidth===0){this.showFallback();}}Alternatively, drop loading="lazy" from avatar_image.rb, or hide via opacity/visibility (which do not stop lazy loading) instead of the hidden (display:none) class.
Happy to open a PR if a preferred direction is confirmed.
Summary
In
RubyUI::Avatar, an uncached avatar image can get permanently stuck on the fallback and never appear. Theavatar_controllerhides the still-loading<img>viadisplay:noneonconnect(), but the image is rendered withloading="lazy". Browsers do not loadloading="lazy"images whose box is not generated (display:none), so theloadevent never fires and the controller never reveals the image.ruby_ui 1.3.0lib/ruby_ui/avatar/avatar_controller.js,lib/ruby_ui/avatar/avatar_image.rbRelevant code
avatar_image.rbrenders the image as lazy:avatar_controller.jshides any image that isn't already complete:Why it breaks
connect()the image is not yetcomplete, soshowFallback()runs and addshidden→display:noneto the<img>.loading="lazy"and nowdisplay:none, the browser defers/skips its load (per the lazy-loading spec, images that generate no box are not loaded until shown).loadevent therefore never fires, soshowImage()is never called and the image stays hidden indefinitely — only the fallback is visible.This is a self-contradiction in the component:
loading="lazy"asks the browser to defer loading until the element is visible, whileconnect()makes the element invisible before it has loaded.Steps to reproduce
Avatarwith anAvatarImagepointing at an uncached URL (hard refresh / disable cache).imageTarget.completeis already true atconnect().Suggested fix
Don't hide a still-loading image. Only fall back when the image has definitively failed (already complete with zero natural size); leave loading/successful images visible and let the
load/erroractions handle later transitions:Alternatively, drop
loading="lazy"fromavatar_image.rb, or hide viaopacity/visibility(which do not stop lazy loading) instead of thehidden(display:none) class.Happy to open a PR if a preferred direction is confirmed.