useCwaFileField
useCwaFileField resolves one uploadable file field on a component backed by a PHP entity with #[Silverback\Uploadable]. It returns the display media, its content URL, and load state for that field. Because you name the field at the call site, one component can wire several file fields without them colliding.
It's the standalone form of the withFile() plugin — the same resolver under the hood, called directly instead of through useCwaComponent's plugin array.
useCwaImageResource (removed). The file APIs handle any uploadable file, not just images — imagineFilterName is an image-specific convenience.import { useTemplateRef } from 'vue'
import { useCwaFileField } from '#imports'
import type { IriProp } from '#cwa/composables/cwa-resource'
const props = defineProps<IriProp>()
const imageRef = useTemplateRef<unknown>('file')
const { contentUrl, displayMedia, handleLoad, loaded } = useCwaFileField(props, {
imagineFilterName: 'thumbnail',
imageRef
})
Signature
useCwaFileField(
props: IriProp, // your component's props (with `iri`)
fileOps?: {
fileProp?: string // the PHP #[UploadableField] property (default: 'file')
imagineFilterName?: string // Imagine filter variant to surface as displayMedia
imageRef?: Readonly<ShallowRef<unknown>> // opt in to cached-image load detection
}
)
- Pass
propsdirectly — no need totoReftheiri; the composable resolves the resource itself. fileProp— which uploadable field to read, i.e. which key of_metadata.mediaObjectsto resolve. Defaults to'file'.imagineFilterName— the LiipImagineBundle filter to surface asdisplayMedia/contentUrl(e.g.'thumbnail','hero'). Omit for the original uploaded file.imageRef— a template ref pointing at the<img>that renders this file. Optional, and never registered for you. See below.
Why imageRef exists
An <img> served from cache can finish loading before Vue attaches the @load listener. When that happens @load never fires, loaded stays false forever, and any placeholder gated on it is stuck over a fully-loaded image.
imageRef exists solely to close that gap: on mount, the composable reads the element's own load state (complete / naturalHeight) and calls handleLoad() itself if the image is already there. That is its only job — contentUrl and displayMedia come from resource data and never touch it.
So the rule is simple:
- Rendering an
<img>and want the cached case handled? PassimageRef. - Anything else — a PDF, a zip, any non-image field — omit it. Only an
<img>can report its own load state, so@loadis the sole source of truth. A ref that can't report it (a<div>, an unmatched ref name) is ignored at runtime.
Both a bare <img> and a component wrapping one (<NuxtImg>) work — a component ref resolves to the instance and is unwrapped to its root <img>.
imageRef used to be registered implicitly from fileProp via useTemplateRef(fileProp). It no longer is — if you relied on load detection, you must now pass the ref yourself, or loaded will never flip for a cached image and your placeholder will never fade.// before — ref auto-registered from fileProp
const { loaded } = useCwaFileField(props, { imagineFilterName: 'thumbnail' })
// after — register it yourself
const imageRef = useTemplateRef<unknown>('file')
const { loaded } = useCwaFileField(props, { imagineFilterName: 'thumbnail', imageRef })
'file' and collided — TypeError: Cannot redefine property: file, thrown only in production builds (dev merely warned). It also only ever worked if you happened to name your template ref exactly the fileProp, and did nothing at all otherwise.fileProp no longer has any relationship to a template ref name — name the ref whatever you like. ref="file" is convention, not a requirement.
Return values
| Return | Type | Purpose |
|---|---|---|
contentUrl | ComputedRef<string | undefined> | URL of the (optionally filtered) file |
displayMedia | ComputedRef<MediaFile | undefined> | The media object to display — has contentUrl, mimeType, width, height; undefined until available |
handleLoad | () => void | Call on the <img>'s @load event |
loaded | Ref<boolean> | true once handleLoad() fires |
Example
<template>
<div class="relative">
<NuxtImg
v-show="loaded"
ref="file"
:src="contentUrl"
:width="displayMedia?.width"
:height="displayMedia?.height"
class="w-full object-cover"
@load="handleLoad"
/>
<div v-if="!loaded" class="skeleton h-48 w-full bg-gray-200 animate-pulse" />
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import { useCwaFileField } from '#imports'
import type { IriProp } from '#cwa/composables/cwa-resource'
const props = defineProps<IriProp>()
const imageRef = useTemplateRef<unknown>('file')
const { contentUrl, displayMedia, handleLoad, loaded } = useCwaFileField(props, {
imagineFilterName: 'thumbnail',
imageRef
})
</script>
v-show, not v-if, when gating the image on loaded.loaded is set by that element's own @load, so v-if="loaded" is a deadlock — the element is never created, so it never loads, so loaded never becomes true. v-show keeps it in the DOM so it can load and fade in.Type the ref as unknown
Declare the ref as useTemplateRef<unknown>('file'). Without the annotation, useTemplateRef infers its type from the template — and if the template also reads this composable's own result, that inference is circular and vue-tsc fails:
TS7022: 'imageRef' implicitly has type 'any' because it does not have a type annotation
and is referenced directly or indirectly in its own initializer.
unknown is also honest: on a component like <NuxtImg> the ref's value is the component instance, not an HTMLImageElement.
Multiple file fields
Call it once per field, naming each with fileProp:
const hero = useCwaFileField(props, { fileProp: 'heroImage' })
const thumb = useCwaFileField(props, { fileProp: 'thumbnail' })
// hero.contentUrl, thumb.contentUrl, …
Each call needs its own imageRef (pointing at its own template ref) if you want load detection for that field — they're independent.
If you're already calling useCwaComponent, prefer the plugin form — useCwaComponent(props, [withFile({ fileProp: 'heroImage' }), withFile({ fileProp: 'thumbnail' })]) exposes the same data under a files map.