CWA 2 is a prerelease (2.0.0-alpha), heading for a beta. It runs production sites; configuration can still change between releases, always with upgrade notes. View versions and support
Component

useCwaFileField

Resolve a single uploadable file field on a resource — content URL, display media, load state, and Imagine filter variants — named at the call site.

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.

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
  }
)

Parameters

ParameterTypeDescription
propsIriPropYour component's props, passed directly. No toRef needed; the composable resolves the resource itself
fileOps.filePropstringWhich uploadable field to read, i.e. which key of _metadata.mediaObjects to resolve. Default 'file'
fileOps.imagineFilterNamestringThe LiipImagineBundle filter to surface as displayMedia/contentUrl (e.g. 'thumbnail', 'hero'). Omit for the original uploaded file
fileOps.imageRefReadonly<ShallowRef<unknown>>A template ref pointing at the <img> that renders this file. Optional, and never registered for you. See Why imageRef exists

Return Values

ReturnTypePurpose
contentUrlComputedRef<string | undefined>URL of the (optionally filtered) file
displayMediaComputedRef<MediaFile | undefined>The media object to display — has contentUrl, mimeType, width, height; undefined until available
srcsetComputedRef<string | undefined>Every sized variant of the field, for an <img>'s srcset. See srcset
handleLoad() => voidCall on the <img>'s @load event
loadedRef<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>
Use 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.

srcset

srcset lists the field's variants for a responsive <img>, so the browser can pick the smallest file that fits. Bind it next to a sizes of your own:

  • Each entry is "<url> <width>w", for the original and every Imagine filter with a known width, smallest first, joined with , .
  • Every URL carries the same ?published= query as contentUrl.
  • A variant with no known width is left out. That covers non-image files, an SVG without width attributes, and a variant the API sends with a width of -1 because it has no file info stored.
  • When two variants have the same width, the smaller file is kept. A filter never upscales, so a small original and its filter can match.
  • It is undefined with fewer than two usable variants. That is always the case for an SVG, because the API makes no filters for it. Bind it as :srcset="srcset", and Vue leaves the attribute out when it's undefined.

imagineFilterName still picks contentUrl and displayMedia, so it sets the src. srcset lists every variant, whichever filter you name. See Images & Media › Responsive images for the sizes to use.

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? Pass imageRef.
  • Anything else — a PDF, a zip, any non-image field — omit it. Only an <img> can report its own load state, so @load is 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>.

fileProp has no relationship to a template ref name, so name the ref whatever you like. ref="file" is convention, not a requirement.

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.