useCwaResourceUpload
Use in admin tab components for uploadable components. Not for display components.
The composable returns a ready-to-spread bind object — spread it onto CwaUiFormFile with v-bind and it wires the v-model, fileExists, disabled, and the change/delete events for you. Only label (and optionally accept) stay per-field:
import { useCwaResourceManagerTab, useCwaResourceUpload } from '#imports'
const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Upload' })
const { bind } = useCwaResourceUpload(iri)
defineExpose(exposeMeta)
<template>
<CwaUiFormFile v-bind="bind" label="Upload Image" />
</template>
bind. It's a ComputedRef, and Vue only auto-unwraps refs that are top-level setup bindings. So v-bind="bind" unwraps correctly, but v-bind="upload.bind" (nested access on the composable-return object) does not — it spreads the raw ref and TypeScript complains that modelValue/fileExists are missing. Destructure const { bind } = useCwaResourceUpload(iri), and for multiple fields destructure-and-rename per call (see below).Signature
useCwaResourceUpload(
iri: ComputedRef<string | undefined>,
filename: string = 'file',
fileDisplayType: string = 'File',
ops?: { imageDownscale?: Partial<ImageDownscaleOptions> }
)
iri— the reactive IRI ref fromuseCwaResourceManagerTab. Identifies which resource to PATCH.filename— the PHP entity property name / media object key for the file field. Defaults to'file'. Pass the actual property name if your entity uses a different name (e.g.'image','document').fileDisplayType— noun used in the delete-confirmation dialog copy and in the file input's label for an existing file (e.g.'Image','Document'). Defaults to'File'.ops.imageDownscale— overrides how large images are resized for this field only. See Large images are resized before upload.
Return values
| Return | Type | Purpose |
|---|---|---|
bind | ComputedRef<object> | Spread onto CwaUiFormFile via v-bind — covers v-model, fileExists, disabled, change, delete |
filenameInputModel | Ref<string> | Text shown in the file input (the v-model target inside bind) |
updating | Ref<boolean> | true while an upload/delete is in progress |
fileExists | ComputedRef<boolean> | true when a file is already uploaded for this resource |
handleInputChangeFile | (file: File | undefined) => Promise<void> | Change handler (also inside bind as onChange) |
handleInputDeleteFile | () => Promise<void> | Delete handler (also inside bind as onDelete) |
bind is the recommended path. The individual values remain exported if you need to wire the input manually or drive custom UI.
How it works
handleInputChangeFile — resizes the file first if it is a large image (see below), then sends it to {iri}/upload as multipart/form-data (not base64). Sets updating.value = true during the upload. On success the resource store updates automatically.
handleInputDeleteFile — shows a confirmation dialog, then sends PATCH {iri} with { [filename]: null }. The API deletes the stored file.
filenameInputModel — the text shown in the file input (bound with v-model on CwaUiFormFile). For an already-uploaded file it reads Existing <fileDisplayType> (<size>) — e.g. Existing Image (24.1 kB) — not the stored filename. It is empty when no file exists, and shows the chosen filename immediately after the user picks one.
CwaUiFormFile props
bind supplies the wiring; add label (and optionally accept) per field:
<CwaUiFormFile
v-bind="bind" <!-- v-model, fileExists, disabled, change, delete -->
label="Image"
accept="image/*" <!-- optional MIME restriction -->
/>
Complete admin upload tab example
<!-- app/cwa/components/HeroSection/admin/ImageTab.vue -->
<template>
<CwaUiFormFile v-bind="bind" label="Upload Image" />
</template>
<script setup lang="ts">
import { useCwaResourceManagerTab, useCwaResourceUpload } from '#imports'
const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Upload' })
const { bind } = useCwaResourceUpload(iri)
defineExpose(exposeMeta)
</script>
Multiple file fields
There's no multi-file wrapper — call useCwaResourceUpload once per field, passing each field's property name. Because you can't spread a nested field.bind (it won't unwrap — see the callout above), destructure and rename each bind:
<template>
<CwaUiFormFile v-bind="posterBind" label="Poster" accept="image/*" />
<CwaUiFormFile v-bind="thumbnailBind" label="Thumbnail" accept="image/*" />
</template>
<script setup lang="ts">
import { useCwaResourceManagerTab, useCwaResourceUpload } from '#imports'
const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Media' })
const { bind: posterBind } = useCwaResourceUpload(iri, 'poster')
const { bind: thumbnailBind } = useCwaResourceUpload(iri, 'thumbnail')
defineExpose(exposeMeta)
</script>
Each call reads and writes its own mediaObjects[<property>] key, so two fields on the same resource never couple at the composable level. If fields do appear to share a file, it's a resource-data issue (a lost _metadata.mediaObjects key), not this composable.
Large images are resized before upload
A photo straight from a phone can be 12 to 48 megapixels. The API decodes the whole image to build its Imagine filters, and no layout needs an image that large. So useCwaResourceUpload resizes large images in the browser before it sends them. This is on by default and needs no setup.
@cwa/nuxt-edge 0.0.0-29836514.8704582 or later. Older builds upload the original file.With the defaults, a JPEG, PNG or WebP whose longest edge is over 2560 px, or whose area is over 20 megapixels, is resized to fit both limits. The aspect ratio is kept. The admin sees no notice when this happens.
Options
| Option | Default | Description |
|---|---|---|
enabled | true | false uploads every file unchanged |
thresholdEdge | 2560 | Resize when the longest edge is over this many pixels |
thresholdPixels | 20000000 | Resize when width × height is over this |
maxEdge | 2560 | Longest edge of the result |
maxPixels | 20000000 | Width × height of the result |
quality | 0.85 | Encode quality for JPEG and WebP. Browsers ignore it for PNG |
Set these for the whole app with cwa.upload.image in nuxt.config.ts, or for one field with the fourth argument:
// A downloads field that must keep the original file
const { bind } = useCwaResourceUpload(iri, 'file', 'File', { imageDownscale: { enabled: false } })
// A hero image that may be larger
const { bind: heroBind } = useCwaResourceUpload(iri, 'hero', 'Image', { imageDownscale: { maxEdge: 4096 } })
Each option is taken from the call first, then from cwa.upload.image, then from the built-in default. An option set to undefined does not override. So { maxEdge: 4096 } changes only maxEdge.
Thresholds and targets are separate
The threshold options decide whether an image is resized. The max options decide what size it becomes. A file is resized when it is over either threshold, and the result fits both targets.
They default to the same values, so any image over the target is resized. Keeping them separate lets you leave images alone unless they are well over the target. For example, with thresholdEdge: 4000 and maxEdge: 2560, a 3500 px image is sent unchanged and a 5000 px image is resized to 2560 px.
An image that is over a threshold but already within the targets is never enlarged. It is sent unchanged. So with maxEdge: 4096 and the default thresholdEdge, a 3000 px image is sent unchanged.
The edge rule and the pixel rule catch different images. A 48 MP phone photo is over both. A 20000 × 1000 panorama is only 20 MP, so only the edge rule catches it. With the default 2560 px edge, the pixel limit can never apply, because 2560 × 2560 is 6.6 MP. It is there for an app that raises maxEdge.
maxEdge or maxPixels, check that the API still accepts the result. If you lower the API's limits, lower these too. See Large Images and PHP Memory.What is kept and what is not
- The format is kept. A JPEG stays a JPEG and a PNG stays a PNG, so transparency survives. The file name is kept too.
- The original is sent if resizing doesn't make it smaller. For example, a palette PNG can grow when it is re-encoded.
- SVG, GIF and animated WebP are never resized. Nor is any other type, such as AVIF or a PDF. Only JPEG, PNG and still WebP are resized.
- An upload never fails because of this. If the browser can't decode or encode the image, the original is sent.
- EXIF metadata is removed from resized images. The photo is rotated to match its EXIF orientation first. The new file has no EXIF, so GPS location and camera details from phone photos are not published. That is good for privacy. If you need that metadata, turn resizing off for the field. Images that are not resized keep their metadata.
The API's own limits still apply. They also protect uploads that don't come through the admin, such as a direct API request.