Skip to content

Commit 8208e4c

Browse files
committed
Only auto-load a file field when the ref is a genuinely loaded img (#267)
`useCwaFile`'s mount-time "already loaded" shortcut read: if (ops.imageRef.value?.complete || ops.imageRef.value?.naturalHeight !== 0) The ref is not always a bare <img>. `ref="file"` on a COMPONENT — as the playground's Image.vue does with <NuxtImg ref="file"> — resolves to the component instance, not the element. A ref name that doesn't match `fileProp` resolves to null. And a file field needn't be an image at all. In every one of those cases `naturalHeight` is `undefined`, and `undefined !== 0` is TRUE, so `handleLoad()` fired on mount and `loaded` was true from the start — defeating the whole load-detection mechanism. Visibly, the placeholder vanished instantly and the image popped in unfaded, because nothing was waiting for @load. Unwrap a component instance to its root element, and only apply the heuristic to something that can actually report img load state. Anything else waits for @load, which still fires normally. This is section 3 of #267. The `Cannot redefine property` crash and the implicit-useTemplateRef design question are separate and remain open. The existing `complete || naturalHeight !== 0` heuristic is deliberately left as-is rather than re-litigated here — the fix is scoped to what the ref resolves to, not to how a loaded img is recognised.
1 parent 698e89f commit 8208e4c

2 files changed

Lines changed: 52 additions & 1 deletion

File tree

src/runtime/composables/cwa-file.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,43 @@ describe('useCwaFile', () => {
137137
const { loaded } = useCwaFile(iri, ops)
138138
expect(loaded.value).toBe(false)
139139
})
140+
141+
/**
142+
* The ref is not always a bare `<img>`. `ref="file"` on a COMPONENT (`<NuxtImg ref="file">`, as
143+
* the playground uses) resolves to the component instance; a ref name that doesn't match
144+
* `fileProp` resolves to null; a file field needn't be an image at all.
145+
*
146+
* In every one of those cases `naturalHeight` is `undefined`, and `undefined !== 0` is TRUE — so
147+
* `loaded` flipped true on mount, before the image had loaded, and the placeholder vanished
148+
* instantly. Anything we cannot positively identify as a loaded <img> must wait for `@load`.
149+
*/
150+
describe('when the ref is not a bare img element', () => {
151+
test('resolves a component instance to its root element and respects its load state', () => {
152+
// <NuxtImg ref="file"> — useTemplateRef gives the component, whose $el is the real <img>
153+
const component = { $el: { complete: false, naturalHeight: 0 } }
154+
const ops = makeOps({ imageRef: ref(component as any) })
155+
const { loaded } = useCwaFile(iri, ops)
156+
expect(loaded.value).toBe(false)
157+
})
158+
159+
test('a component instance whose image is already loaded still auto-loads', () => {
160+
const component = { $el: { complete: true, naturalHeight: 100 } }
161+
const ops = makeOps({ imageRef: ref(component as any) })
162+
const { loaded } = useCwaFile(iri, ops)
163+
expect(loaded.value).toBe(true)
164+
})
165+
166+
test('does not auto-load when the ref is missing (e.g. ref name does not match fileProp)', () => {
167+
const ops = makeOps({ imageRef: ref(null) as any })
168+
const { loaded } = useCwaFile(iri, ops)
169+
expect(loaded.value).toBe(false)
170+
})
171+
172+
test('does not auto-load when the ref is a non-image element', () => {
173+
const ops = makeOps({ imageRef: ref({ tagName: 'DIV' } as any) })
174+
const { loaded } = useCwaFile(iri, ops)
175+
expect(loaded.value).toBe(false)
176+
})
177+
})
140178
})
141179
})

src/runtime/composables/cwa-file.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,21 @@ export const useCwaFile = (iri: Ref<string>, ops: FileOpsType): CwaFileReturnTyp
6060
return `${mediaUrl}${query.value}`
6161
})
6262

63+
// An <img> that was already loaded (from cache) before the `@load` listener was attached never
64+
// fires it, so detect that on mount. The ref is not necessarily a bare <img> though: `ref="file"`
65+
// on a COMPONENT (`<NuxtImg ref="file">`) resolves to the component instance, a ref name that
66+
// doesn't match `fileProp` resolves to null, and a file field needn't be an image at all. In each
67+
// of those `naturalHeight` is `undefined` — and `undefined !== 0` is TRUE, which flipped `loaded`
68+
// on mount before the image had loaded and made the placeholder vanish instantly.
6369
onMounted(() => {
64-
if (ops.imageRef.value?.complete || ops.imageRef.value?.naturalHeight !== 0) {
70+
const target = ops.imageRef.value as { $el?: unknown } | null
71+
// unwrap a component instance to its root element
72+
const el = (target && '$el' in target ? target.$el : target) as HTMLImageElement | null
73+
// Only an <img> can report its own load state. Anything else must wait for `@load`.
74+
if (!el || typeof el.naturalHeight !== 'number') {
75+
return
76+
}
77+
if (el.complete || el.naturalHeight !== 0) {
6578
handleLoad()
6679
}
6780
})

0 commit comments

Comments
 (0)