camelon

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}` : '' });
}
service worker

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',
    });
  }
}
service worker

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' },
  });
}