sercrod

*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:

Behavior

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:

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:

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:

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

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

  1. Sercrod renders the element and binds *upload.
  2. The user activates the element by click or keyboard.
    The hidden file input is programmatically clicked and the file picker dialog appears.
  3. On file selection, the change handler fires:
    • If there are no files (user cancels), nothing happens.
    • Otherwise Sercrod emits sercrod-upload-start, builds a FormData, and calls _xhr_upload.
  4. _xhr_upload wires up:
    • Progress events (XMLHttpRequest.upload.onprogress) to emit sercrod-upload-progress.
    • Completion to either resolve with {status, body} or reject with an error.
  5. On success, Sercrod:
    • Emits sercrod-uploaded.
    • Writes the response body into data (see "Variable creation and *into").
    • Schedules a re-render via update(true).
  6. After the render cycle finishes, Sercrod internal _finalize runs:
    • It resets $upload and $download to null.
    • It clears any variables that were registered via *into for this cycle by setting them to null.
    • It leaves the rest of the data object untouched.

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.

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:

*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.

This pattern keeps the responsibility of each directive clear and predictable.

Events and UI integration

*upload is designed to be driven from events:

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:

Recommended approach on the server:

Benefits:

Best practices

Notes