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:
def default_attrs
{
loading: "lazy",
data: {
ruby_ui__avatar_target: "image",
action: "load->ruby-ui--avatar#showImage error->ruby-ui--avatar#showFallback"
},
...
}
end
avatar_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.