*upload
Summary
*upload turns any element into an accessible file-upload trigger.
It creates a hidden <input type="file"> next to the element, lets the user pick one or more files, and sends them to a server endpoint using XMLHttpRequest plus FormData.
On success, the response is stored into $upload and optionally into a named variable via *into, then a re-render is triggered.
Basic example
Upload a single file to /api/upload and capture the JSON response into result:
<serc-rod data='{"result": null}'>
<button *upload="'/api/upload'" *into="result">
Upload file...
</button>
<p *if="result">
<span *print="result.message"></span>
</p>
</serc-rod>
Key points:
- The
*uploadvalue is a Sercrod expression. In this basic form it evaluates to a string URL. - When the upload finishes successfully, the response body is assigned to
resultbecause of*into="result". - The same response is also exposed through
$uploadas a global one-shot value.
Behavior
- When Sercrod sees
*upload(or its aliasn-upload) on an element, it:- Clones the element (without children).
- Evaluates the
*uploadexpression in the current effective scope. - Normalizes the result into an option object
{ url, method, field, with, headers, credentials }. - Ensures the clone is keyboard-clickable (through role and
tabindex) if it was not already. - Creates (or reuses) a hidden
<input type="file" data-sercrod-generated="1">as a child of the element. - Mirrors the element attributes
accept,multiple, andcaptureonto the hidden input. - Registers
clickandkeydownhandlers that open the file picker. - Registers a
changehandler on the hidden input that starts the upload when files are selected.
- When the user selects files and confirms:
- Sercrod dispatches a
sercrod-upload-startevent on the host<serc-rod>withdetail:{host, el, files, url, with}. - Sercrod sends the files to the configured
urlusingXMLHttpRequestandFormData. - As the upload progresses,
sercrod-upload-progressevents are dispatched withdetail:{host, el, loaded, total, percent}wheneverlengthComputableis true. - When the upload finishes, Sercrod:
- Parses the text response as JSON if possible; otherwise keeps it as a string.
- Dispatches
sercrod-uploadedwithdetail:{host, el, response, status}. - Stores the response into
$uploadand/or the*intotarget (described below).
- Sercrod dispatches a
- If anything goes wrong:
- During initial option evaluation or setup, Sercrod dispatches
sercrod-errorwithdetail:{host, el, stage:"upload-init", error}. - During the network request, Sercrod dispatches
sercrod-errorwithdetail:{host, el, stage:"upload", error}.
- During initial option evaluation or setup, Sercrod dispatches
The *upload directive itself does not submit any surrounding <form> element and does not use fetch. It always uses XMLHttpRequest so that upload progress events are available.
Option object
The *upload value expression must evaluate to either:
- A string
- Interpreted as
{ url: "<that string>" }
- Interpreted as
- An object
-
Normalized to:
url(required) - Target URL for the upload.method(optional) - HTTP method, defaults to"POST".field(optional) - Form field name for files, defaults to"file".with(optional) - Plain object of extra fields to append to theFormData.headers(optional) - Extra request headers (for example CSRF tokens).credentials(optional) - When truthy, enablesxhr.withCredentials.
-
If the resolved value is not a string or an object with url, Sercrod throws inside _normalize_upload_opts and emits a sercrod-error with stage:"upload-init".
Files are added to FormData as follows:
- For a single file:
fd.append(field, file). - For multiple files:
fd.append(field + "[0]", file0),fd.append(field + "[1]", file1), and so on.
Extra keys from with are appended directly to the same FormData instance.
You normally do not need to set a Content-Type header: XMLHttpRequest automatically sets the appropriate multipart boundary when sending FormData. If you do specify Content-Type yourself in headers, it will override this default, so use that only if you know exactly what you are doing.
Hidden input and host attributes
*upload always works via a hidden file input placed as a child of the element that carries *upload:
-
The hidden input is created lazily once and reused on subsequent re-renders.
-
It is positioned far off-screen using fixed positioning so it does not affect layout.
-
The element attributes are mirrored:
accepton the element becomesaccepton the hidden input.multipleon the element becomesmultipleon the hidden input.captureon the element becomescaptureon the hidden input.
When Sercrod re-binds the same DOM element (for example after an update where the node is reused), the options and the mirrored attributes on the hidden input are updated, but existing event listeners are reused.
Evaluation timing
-
The
*uploadexpression is evaluated during binding:- On the first render of the element with
*upload. - On subsequent updates when the same DOM element is re-bound (for example when its
*uploadvalue or other data dependencies change but the element itself is reused).
- On the first render of the element with
-
The expression is not re-evaluated on every click.
To change the upload target or options dynamically, update your data and let Sercrod re-render so that_bind_uploadruns again and refreshes the options.
Any error during expression evaluation is reported through sercrod-error (stage:"upload-init") and prevents the upload handler from being configured or refreshed.
Execution model
- Sercrod renders the element and binds
*upload. - The user activates the element by click or keyboard.
The hidden file input is programmatically clicked and the file picker dialog appears. - On file selection, the
changehandler fires:- If there are no files (user cancels), nothing happens.
- Otherwise Sercrod emits
sercrod-upload-start, builds aFormData, and calls_xhr_upload.
_xhr_uploadwires up:- Progress events (
XMLHttpRequest.upload.onprogress) to emitsercrod-upload-progress. - Completion to either resolve with
{status, body}or reject with an error.
- Progress events (
- On success, Sercrod:
- Emits
sercrod-uploaded. - Writes the response body into data (see "Variable creation and *into").
- Schedules a re-render via
update(true).
- Emits
- After the render cycle finishes, Sercrod internal
_finalizeruns:- It resets
$uploadand$downloadtonull. - It clears any variables that were registered via
*intofor this cycle by setting them tonull. - It leaves the rest of the data object untouched.
- It resets
The upload is purely client-side. Sercrod does not retry failed uploads and does not perform automatic backoff. Such policies should be implemented on top using the exposed events.
Variable creation and *into
*upload does not create any loop variables or local aliases.
Instead, it writes to the data object when an upload completes.
-
Default behavior (no
*into):- The response body is stored in
$upload. $uploadis then reset tonullby_finalizeafter the render cycle completes.- This makes
$uploada convenient "last upload result" scratch space.
- The response body is stored in
-
With
*intoorn-intoon the same element:- Sercrod reads the attribute value (for example
*into="result"). - On success, the response is assigned to
this._data[result]. - The key is recorded internally so that
_finalizecan later clear it by writingnull. $uploadis also populated on the first truthy response, if it was not already set.
- Sercrod reads the attribute value (for example
In other words, *into provides a one-shot local variable for the response, while $upload is a shared, short-lived global.
Example:
<serc-rod data='{"profile": null}'>
<button *upload="'/api/profile/upload-avatar'" *into="profile">
Upload avatar
</button>
<div *if="profile">
<p>Avatar updated.</p>
<p *print="profile.url"></p>
</div>
</serc-rod>
If you need to persist the response beyond a single render cycle, copy it from $upload or the *into target into a more permanent field (for example state.last_upload) inside an event handler or a computed expression.
Scope layering and parent access
The *upload expression is evaluated in the same effective scope as other data directives:
- It sees the current host data.
- It sees variables introduced by surrounding
*let. - Inside loops (
*for,*each), it sees the loop variables for the current iteration. - It can access outer data through normal property access (for example
parent.user.idif you exposedparentyourself).
*upload does not change the scope for its children: the element content is rendered with the same scope that was used to evaluate the *upload expression.
Use with conditionals and loops
*upload can be combined with conditional rendering and loops, with a few points to keep in mind.
-
Wrapping in
*if:-
It is often useful to show or hide the upload button based on state:
<button *if="can_upload" *upload="'/api/upload'"> Upload file </button> -
When the
*ifcondition switches from false to true, the element is re-created and*uploadis bound again, re-evaluating its expression.
-
-
Inside
*foror*each:-
You can generate multiple upload buttons from a list:
<button *each="folder in folders" *upload="{ url: '/api/upload', with: { folder_id: folder.id } }" > Upload to <span *print="folder.name"></span> </button> -
Each instance gets its own hidden input and its own options.
Uploaded files for each button update data independently (often via different*intotargets).
-
-
Combining with other control directives on the same element:
-
As with other Sercrod directives, only one structural control branch is applied per element during rendering.
-
In practice, you should avoid mixing
*uploadon the same element with other directives that also want to own rendering (for example*template,*include,*import). -
Use wrapping elements instead:
<div *if="ready"> <button *upload="'/api/upload'">Upload</button> </div>
-
This pattern keeps the responsibility of each directive clear and predictable.
Events and UI integration
*upload is designed to be driven from events:
-
sercrod-upload-start- Fired on the host<serc-rod>when an upload begins.detail.host- The host element instance.detail.el- The element that has*upload.detail.files- The selected files.detail.urlanddetail.with- The resolved URL and extra payload.
-
sercrod-upload-progress- Fired as the upload proceeds (when the browser can compute total size).detail.loaded/detail.total- Bytes sent vs total.detail.percent- Rounded percentage from 0 to 100.
-
sercrod-uploaded- Fired when the upload completes successfully.detail.response- Parsed JSON or plain string body.detail.status- HTTP status code.
-
sercrod-error- Fired on errors.detail.stageis"upload-init"if the options could not be evaluated or normalized.detail.stageis"upload"for network or HTTP-level errors.
You can handle these events using Sercrod event attributes on the element with *upload. For example:
<button
*upload="'/api/upload'"
@sercrod-upload-start="log('upload started', $event.detail)"
@sercrod-upload-progress="progress = $event.detail.percent"
@sercrod-uploaded="last_result = $event.detail.response"
>
Upload file
</button>
This lets you drive progress bars, disable other controls while an upload is active, or copy the response into long-lived state.
Server-side contract for *upload
*upload is slightly different from *post, *fetch, and *api on the request side, but it benefits from the same “Sercrod API style” on the response side.
Server-side expectations:
-
Request shape:
- Sercrod always sends files using
multipart/form-dataviaXMLHttpRequestandFormData. - Files are placed under a configurable field name:
- Default:
"file". - Custom: the
fieldproperty of the*uploadoption (for examplefield: "avatar").
- Default:
- When multiple files are allowed, Sercrod appends them as
field[0],field[1], and so on. - Any extra data passed through the
withoption is appended to the sameFormDataas simple text fields.
- Sercrod always sends files using
-
Response shape:
- The HTTP response body is read as text and then:
- Parsed as JSON when possible.
- Left as a plain string when JSON parsing fails.
- The resulting value is stored directly into:
$upload(global, short-lived).- And, if
*into="name"is present,data[name]for the current host.
- There is no
URL[:prop]shorthand for*upload. If you want to expose only a particular property, design your JSON envelope accordingly or copy the property into another field after the upload.
- The HTTP response body is read as text and then:
Recommended approach on the server:
-
Treat
*uploadendpoints as file-plus-metadata variants of the same Sercrod API style:- Accept
multipart/form-datawith:- One or more file fields under a known field name.
- Optional additional fields corresponding to the
withpayload.
- Always return a JSON response for both success and application-level errors.
- Reuse the same JSON envelope that you use for
*post/*fetch/*api, so that*intoand$uploadcan be wired consistently.
- Accept
Benefits:
-
You can use a single “Sercrod API style” on the backend:
- File uploads (
*upload) and pure JSON calls (*post,*fetch,*api) share the same response contract. - Monitoring and logging can treat all Sercrod endpoints uniformly.
- Frontend code can handle upload results in the same way it handles other API responses.
- File uploads (
-
For existing upload endpoints:
- You can typically integrate by:
- Matching the expected field name using the
fieldoption. - Adding any legacy flags or identifiers through the
withoption. - Adjusting the handler to always return a JSON envelope.
- Matching the expected field name using the
- This lets you gradually align older upload handlers with the Sercrod API style without breaking existing behavior.
- You can typically integrate by:
Best practices
- Prefer server endpoints that accept
multipart/form-dataand do not require you to manually craftContent-Typeheaders. - Use the element
acceptattribute to restrict selectable file types (for exampleaccept="image/*"). - Add
multiplewhen you want to allow multiple files in a single upload. - Add
capturefor camera or microphone capture on supporting mobile browsers. - Keep the upload element simple and clearly labeled so that users discover it easily.
- Treat
*intoand$uploadas short-lived slots: copy anything you need to preserve into stable data fields. - Handle errors via
@sercrod-erroron the host or by listening forsercrod-errorand showing user-friendly messages.
Notes
- Alias attribute
n-uploadbehaves identically to*uploadand exists for environments where*is inconvenient in attribute names. *uploadusesXMLHttpRequestinstead offetchso that upload progress is observable. You should not mix this with separate manualfetchlogic for the same file input; keep the flow inside Sercrod.*uploaddoes not submit a surrounding<form>. If you need to send other form fields along with the files, pass them through thewithoption or design a dedicated endpoint that accepts both.- The hidden file input is internal to Sercrod. Do not try to style or access it directly; always wire your UI and logic to the element that carries
*upload.