Skip to content

Avatar: lazy-loaded image never appears because connect() hides it with display:none #415

@stadia

Description

@stadia

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

  1. On first (uncached) render, at connect() the image is not yet complete, so showFallback() runs and adds hiddendisplay:none to the <img>.
  2. 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).
  3. 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

  1. Render an Avatar with an AvatarImage pointing at an uncached URL (hard refresh / disable cache).
  2. Observe: fallback shows, real image never appears. Network tab shows the image request is never made (or deferred forever).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions