# Vue

> A Vue 3 single-file component that collects on submit and sends the result with the form.

Source: https://whiskerjs.com/guides/vue/ (last updated 2026-10-26)

> **In plain words:**
>
> This is a normal Vue sign-in form. When it is submitted it fetches a one-time code from your server, lets WhiskerJS read the browser, and posts the result together with the email and password. WhiskerJS is created the first time it is needed and only ever runs in the browser.

## Install: build from source

WhiskerJS is not on npm yet; the npm package is coming. Build it and install the tarball:

```sh
cd whiskerjs
pnpm install && pnpm build && pnpm pack    # writes whiskerjs-0.1.0.tgz
cd ../my-vue-app
npm install ../whiskerjs/whiskerjs-0.1.0.tgz
```

## The component

`src/LoginForm.vue`:

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { load, type Agent } from 'whiskerjs';

const email = ref('');
const password = ref('');
const status = ref('');
const busy = ref(false);

// One agent per page, created on first use. load() is cheap and keeps no state.
let agent: Promise<Agent> | undefined;

async function onSubmit() {
  busy.value = true;
  try {
    // A fresh nonce from your server, then collect. collect() never throws.
    const binding = await (await fetch('/api/whisker-nonce')).json();
    const device = await (await (agent ??= load())).collect({ timeoutMs: 3000, binding });
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ email: email.value, password: password.value, device }),
    });
    status.value = response.ok ? 'Signed in' : `Refused (${response.status})`;
  } catch {
    status.value = 'Network error, please try again';
  } finally {
    busy.value = false;
  }
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <label>Email <input v-model="email" name="email" type="email" required /></label>
    <label>Password <input v-model="password" name="password" type="password" required /></label>
    <button type="submit" :disabled="busy">Sign in</button>
    <output id="status">{{ status }}</output>
  </form>
</template>
```

Mounted as the app’s root component in `src/main.ts`:

```ts
import { createApp } from 'vue';
import LoginForm from './LoginForm.vue';

createApp(LoginForm).mount('#app');
```

## How it works

- **`@submit.prevent`** stops the browser’s own form post, so the handler can add the device result first.
- **The agent** is created by the first submit (`agent ??= load()`) and reused after that. `load()` is cheap and keeps no state between `collect()` calls.
- **The binding.** `/api/whisker-nonce` returns `{ nonce, timestamp }`; `collect({ binding })` echoes both in `result.binding` and adds a digest over the whole result. Your server checks it: [Node](https://whiskerjs.com/guides/verify-node/), [Go](https://whiskerjs.com/guides/verify-go/), [PHP](https://whiskerjs.com/guides/verify-php/), [Python](https://whiskerjs.com/guides/verify-python/).
- **Errors.** `collect()` never throws or rejects; the `try` block covers your two `fetch` calls.
- **`busy`** disables the button while a submit is in flight, so a double click does not use two nonces.

## Things to know

- **Server rendering (Nuxt and similar).** The component’s `script setup` runs on the server when the page is rendered there. Calling WhiskerJS only from the submit handler (or from `onMounted`) keeps it in the browser.
- **Secure context.** Use https, or `localhost` with the Vite dev server. Without Web Crypto the ids are `null`.
- **Send the result unchanged** when the server verifies the binding: the digest covers every field.

## Tested

Tested with WhiskerJS 0.1.0 (signalsVersion 5, idVersion 2), Vue 3.5.44, Vite 8.3.5 with @vitejs/plugin-vue 6.0.10, and vue-tsc 3.3.13 on 2026-10-26: built with `vue-tsc --noEmit && vite build`, served by the [Node server](https://whiskerjs.com/guides/verify-node/#a-complete-server) and submitted in headless Chromium 140 (Playwright 1.55.0). The server accepted the binding and refused the same result sent a second time.

## Questions and answers

### Should the agent be reactive state?

No. The agent is a plain object with one method; keeping it in a ref adds nothing. A plain variable in script setup holds it for the life of the component.

### What about Nuxt?

Nuxt renders components on the server first. Keep load() and collect() inside event handlers or onMounted, which only run in the browser, as this component does. The component itself was tested with Vue and Vite only.

### Do I need a plugin or a global property?

No. Import load where you need it. There is nothing to install into the Vue app.

## Related pages

- [Verify in Node](https://whiskerjs.com/guides/verify-node/): The server this component was tested against.
- [All integration guides](https://whiskerjs.com/guides/): The page-to-server contract.
- [Svelte](https://whiskerjs.com/guides/svelte/): The same form in Svelte 5.
- [Getting started](https://whiskerjs.com/docs/getting-started/): Build WhiskerJS from source.
