Images & Media
When a PHP component uses #[Silverback\Uploadable], use useCwaComponent with the withFile() plugin. It exposes the uploaded file's URL, load state, and Imagine filter variants under a files map keyed by the field's fileProp (default 'file').
withFile() replaces the removed withImage(). The old plugin returned flat contentUrl/displayMedia/… keys; withFile() nests them under files.<fileProp>. Map entries are reactive, so in templates you use them without .value (files.file.contentUrl).Display Component
<!-- app/cwa/components/Image/Image.vue -->
<template>
<div class="relative">
<Transition name="fade">
<NuxtImg
v-if="files.file?.displayMedia"
ref="file"
:src="files.file.contentUrl"
:width="files.file.displayMedia.width"
:height="files.file.displayMedia.height"
class="w-full h-full object-cover"
@load="files.file.handleLoad"
/>
</Transition>
<div
v-if="!files.file?.displayMedia"
class="w-full h-64 bg-gray-200 animate-pulse rounded"
/>
</div>
</template>
<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaComponent, withFile } from '#imports'
const props = defineProps<IriProp>()
const { resource, exposeMeta, files } = useCwaComponent(props, [withFile()])
defineExpose(exposeMeta)
</script>
The ref="file" matches the default fileProp, so the plugin can detect an already-cached image on mount.
The files Map
Each entry — files[fileProp] — carries these values for that field:
| Value | Type | Description |
|---|---|---|
contentUrl | string | undefined | Public URL of the (optionally filtered) file |
displayMedia | MediaFile | undefined | The media object to display — has contentUrl, mimeType, width, height; undefined until available |
loaded | boolean | Becomes true when handleLoad() is called |
handleLoad | () => void | Call on the <img> element's @load event |
Because entries are reactive, use them directly in templates (files.file.contentUrl) — no .value. Gate rendering on files.file?.displayMedia so the skeleton shows until the media is resolved and the browser has loaded the image.
Using a Specific Imagine Filter
Pass imagineFilterName to surface a specific image variant as displayMedia/contentUrl:
const { files } = useCwaComponent(props, [
withFile({ imagineFilterName: 'thumbnail' })
])
// files.file.contentUrl → the 'thumbnail' variant
Without imagineFilterName, the field resolves to the original uploaded file.
Multiple File Fields
A component can hold more than one uploadable field (e.g. a poster and a thumbnail). Add one withFile() per field, each with its own fileProp — useCwaComponent accumulates them into the same files map:
const { files } = useCwaComponent(props, [
withFile({ fileProp: 'poster' }),
withFile({ fileProp: 'thumbnail', imagineFilterName: 'small' }),
])
// files.poster.contentUrl · files.thumbnail.contentUrl
Each fileProp must match a distinct #[UploadableField] property on the PHP entity. For the standalone, non-plugin form, see useCwaFileField.
Admin Upload Tab
useCwaResourceUpload returns a bind object — spread it onto CwaUiFormFile with v-bind and only supply the label (and optional accept) per field:
<!-- app/cwa/components/Image/admin/Image.vue -->
<template>
<div class="p-4">
<CwaUiFormFile v-bind="upload.bind" label="Upload Image" accept="image/*" />
</div>
</template>
<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaResourceManagerTab, useCwaResourceUpload } from '#imports'
defineProps<IriProp>()
const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Image', order: 1 })
const upload = useCwaResourceUpload(iri, 'file') // 'file' = the PHP property name
defineExpose(exposeMeta)
</script>
useCwaResourceUpload(iri, propertyName) handles file selection, multipart upload to {iri}/upload, and deletion via PATCH. For multiple file fields, call it once per property. See useCwaResourceUpload for the full reference.
Video, PDF, and Other Files
The same pattern works for any file type — withFile() is not image-specific. For non-image files there's no @load event to wait for, so render as soon as files.file?.contentUrl is available:
<template>
<a v-if="files.file?.contentUrl" :href="files.file.contentUrl" download>
Download PDF
</a>
</template>
<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaComponent, withFile } from '#imports'
const props = defineProps<IriProp>()
const { exposeMeta, files } = useCwaComponent(props, [withFile()])
defineExpose(exposeMeta)
</script>
For video:
<video v-if="files.file?.contentUrl" :src="files.file.contentUrl" controls class="w-full" />
Transition While Loading
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
The <Transition name="fade"> wrapping the <NuxtImg> produces a smooth fade-in once the image loads.