Forms & I/O
A native form POSTs to an action. Read the submitted fields off body.
// routes/contact.tsx
export async function action({ body }) {
await sendMessage(body); // { name, email, message }
throw redirect('/thanks');
}
export default function Contact() {
return (
<form method="post">
<input name="name" />
<input name="email" type="email" />
<textarea name="message"></textarea>
<button>Send</button>
</form>
);
}
Type body with ActionInput to validate it. See typed contracts.
A reactive submit posts the current signals to a resource route, which patches a reply back:
export default function Echo() {
return (
<div class="d-card" signals='{"msg": "", "reply": ""}'>
<div class="d-row">
<input class="d-input" bind:msg="" placeholder="say something" />
<button class="d-btn d-wide" on:click="@post('/echo/submit')">Send</button>
</div>
<p text="$reply"></p>
</div>
);
}import type { RouteArgs } from 'camelon';
export async function action({ stream, signals }: RouteArgs) {
const { msg = '' } = signals as { msg?: string };
stream.patchSignals({ reply: msg ? `You said: ${msg}` : '' });
}File uploads
A multipart/form-data submit lands on files, keyed by field name. Each upload's .data is a Uint8Array.
export async function action({ files }) {
for (const file of files.avatar ?? []) {
await save(file.filename, file.data); // also .mimeType, .truncated
}
}
<form method="post" enctype="multipart/form-data">
<input name="avatar" type="file" />
<button>Upload</button>
</form>
Upload limits
Each uploaded file is capped at 10 MB by default. A file over the cap is truncated and its truncated flag is set. Change the cap with multipart.fileSize:
await createHandler({
routes: manifest,
multipart: { fileSize: 5 * 1024 * 1024 },
});
Streaming responses
Use the stream arg for Server-Sent Events. Keep patching while your generator runs; the stream stays open until the handler returns. patchElements(html, { selector, mode }) with mode: 'append' adds to an element instead of replacing it.
export default function Stream() {
return (
<div class="d-card">
<button class="d-btn d-wide" on:click="@get('/stream/words')">Stream</button>
<p id="out"></p>
</div>
);
}import type { RouteArgs } from 'camelon';
async function* words(): AsyncGenerator<string> {
for (const word of 'words stream in one at a time'.split(' ')) {
await new Promise((r) => setTimeout(r, 130));
yield word;
}
}
export async function loader({ stream }: RouteArgs) {
stream.patchElements('<p id="out"></p>'); // reset
for await (const word of words()) {
stream.patchElements(`<span> ${word}</span>`, {
selector: '#out',
mode: 'append',
});
}
}For full control over status, headers, and body, a resource route can return a raw Response.
export function loader() {
return new Response('hello', {
status: 200,
headers: { 'content-type': 'text/plain' },
});
}