@valfuse-node/vue
@valfuse-node/vue
Vue 3 adapter for
@valfuse-node—useValfuseFormcomposable with reactiveformState, native v-model bindings, and a 1:1 API contract with the React adapter.
npm install @valfuse-node/vue @valfuse-node/form vue
Requires Node.js 22 or newer for the supported package toolchain. The
adapter peer dependency is Vue 3 or newer. It depends on
@valfuse-node/form; install that package directly when your application
imports the schema API.
If your application already uses both React and Vue and wants the umbrella facade, install its framework peers as well:
npm install @valfuse-node/core react react-dom vue
The core root entry re-exports both framework adapters. See the
@valfuse-node/core install and compatibility notes
before using that entry.
The form schema stays framework-neutral and can also be validated by an
Express API through @valfuse-node/express. Vue and Express can consume the
same localization manifest generated by @valfuse-node/localization.
Table of Contents
- Quick Start
useValfuseForm(options)- Reactive
formState form.register(name)— Vue v-model bindingform.handleSubmit(onValid)form.setErrors/form.clearErrorsform.setValue/form.getValue/form.getValuesform.triggerform.watch(...)— multi-overloadform.reset(values?)form.control- Localization Runtime
- Custom Inputs
- API Parity vs React
- Type Reference
- Development Usage
- Release history
- Support
- License
Quick Start
<script setup lang="ts">
import { createSchema } from "@valfuse-node/form";
import { useValfuseForm } from "@valfuse-node/vue";
const schema = createSchema({
email: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "email", error: { message: "Invalid" } },
],
},
password: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "minLength", value: 8, error: { message: "Min 8" } },
],
},
});
type LoginValues = { email: string; password: string };
const form = useValfuseForm<LoginValues>({
schema,
defaultValues: { email: "", password: "" },
});
async function onSubmit(values: LoginValues) {
await loginApi(values);
}
</script>
<template>
<form @submit="form.handleSubmit(onSubmit)">
<input v-bind="form.register('email')" />
<span v-if="form.formState.errors.email">
{{ form.formState.errors.email.message }}
</span>
<input type="password" v-bind="form.register('password')" />
<span v-if="form.formState.errors.password">
{{ form.formState.errors.password.message }}
</span>
<button type="submit" :disabled="form.formState.isSubmitting">Log in</button>
</form>
</template>
useValfuseForm(options)
function useValfuseForm<TFieldValues extends Record<string, unknown>>(
options: UseValfuseFormProps<TFieldValues>
): UseValfuseFormReturn<TFieldValues>;
Options
interface UseValfuseFormProps<TFieldValues> {
schema: ValfuseSchema; // required
defaultValues: TFieldValues; // required (inferred)
mode?: "onSubmit" | "onChange" | "onBlur" | "onTouched" | "all"; // default: "onSubmit"
reValidateMode?: "onChange" | "onBlur" | "onSubmit"; // default: "onChange"
}
| Option | Type | Default | Notes |
|---|---|---|---|
schema |
ValfuseSchema |
— (required) | The rule-based schema from @valfuse-node/form |
defaultValues |
object literal | — (required) | The generic TFieldValues is inferred from this |
mode |
union | "onSubmit" |
When validation first runs |
reValidateMode |
union | "onChange" |
Mode used after the first submit attempt to re-validate fields the user fixes |
Return value
interface UseValfuseFormReturn<TFieldValues> {
formState: ValfuseFormState<TFieldValues>; // reactive
control: ValfuseFormControl<TFieldValues>;
register: (name) => { name; modelValue; "onUpdate:modelValue"; onBlur };
handleSubmit: (onValid) => (e: Event) => Promise<void>;
setErrors: (errors) => void;
clearErrors: (fields?) => void;
setValue: (name, value, options?) => void;
getValue: (name) => TFieldValues[TName];
getValues: () => TFieldValues;
trigger: (name?) => boolean;
watch: ValfuseVueWatchFunction<TFieldValues>;
reset: (values?) => void;
}
Reactive formState
formState is a reactive() proxy — every field is a getter so Vue tracks reads and triggers re-renders.
| Field | Type | Description |
|---|---|---|
errors |
Partial<Record<keyof T, ValfuseFieldError>> |
Current field errors |
isSubmitting |
boolean |
true while the async submit handler is running |
isSubmitted |
boolean |
true after the first submit attempt |
isSubmitSuccessful |
boolean |
true if the most recent submit completed without throwing |
submitCount |
number |
Total submit attempts |
isDirty |
boolean |
true if any field differs from defaultValues |
isValid |
boolean |
true when no errors are present |
dirtyFields |
Partial<Record<keyof T, true>> |
Fields that differ from defaultValues |
touchedFields |
Partial<Record<keyof T, true>> |
Fields the user has blurred |
defaultValues |
Readonly<T> |
The defaults passed at composable initialization |
<template>
<div v-if="form.formState.isSubmitting">Saving…</div>
<div v-if="form.formState.isDirty">Unsaved changes</div>
<pre>{{ form.formState.errors }}</pre>
</template>
form.register(name) — Vue v-model binding
Returns Vue-native v-model props. Use v-bind to spread them onto an element.
<input v-bind="form.register('email')" />
<!--
expands to:
:name="email"
:modelValue="email value"
@update:modelValue="..."
@blur="..."
-->
The shape is { name, modelValue, "onUpdate:modelValue", onBlur }. This is intentionally different from the React adapter — Vue's idiomatic binding is v-model, not onChange.
form.handleSubmit(onValid)
<script setup lang="ts">
const onSubmit = form.handleSubmit(async (values) => {
// values: TFieldValues (already transformed + validated)
await api.save(values);
});
</script>
<template>
<form @submit="onSubmit">…</form>
</template>
The handler:
- Calls
e.preventDefault()on the submit event - Validates the form
- If valid → calls
onValid(values)and setsisSubmitting = truefor the duration - If invalid → sets
formState.errorsand returns
form.setErrors / form.clearErrors
form.setErrors({ email: { message: "Account exists", code: "auth.duplicate" } });
form.clearErrors(); // clear all
form.clearErrors(["email", "password"]); // clear specific fields
form.setErrors accepts a SetErrorsInput — pass either a string ({ email: "Required" }) or a ValfuseError object ({ email: { message, code, type } }). Strings are normalized to objects automatically.
form.setValue / form.getValue / form.getValues
form.setValue("email", "alice@example.com");
form.setValue("email", "alice@example.com", { shouldValidate: true });
const current = form.getValue("email");
const all = form.getValues();
Pass { shouldValidate: true } to validate the field immediately after the
programmatic update. Without it, a stale field error is cleared, matching the
React adapter's behavior.
getValue and getValues are Vue-specific extensions not present in the React adapter. They return snapshots — mutating them has no effect on the form.
form.trigger
form.trigger(); // all fields
form.trigger("email"); // one field
form.trigger(["email", "password"]); // many
// returns boolean — true if all triggered fields are valid
Diff-merges results into formState.errors without clobbering unrelated field errors.
form.watch(...) — multi-overload
const all = form.watch(); // TFieldValues snapshot
const email = form.watch("email"); // TFieldValues["email"]
const pair = form.watch(["email", "name"]); // Array of values
const unsub = form.watch((values, info) => {
// subscribe to all changes
console.log("changed:", info?.name, values);
});
const unsubField = form.watch("email", (value) => {
// legacy: subscribe to one field
console.log("email is now", value);
});
// later
unsub();
unsubField();
Tip: the
watch(callback)form (global subscription) is preferred over the legacywatch(name, callback)form. The legacy form is preserved for backward compatibility.
form.reset(values?)
form.reset(); // back to defaultValues
form.reset({ email: "" }); // partial override
Resets values, errors, touched, dirty, and submission state (isSubmitted, isSubmitSuccessful, submitCount).
form.control
Same shape as the React adapter's control. Pass it to <ValfuseController> for a renderless slot binding or to the typed useValfuseController composable.
form.control._values; // current values
form.control._errors; // current errors
form.control._touchedFields; // ReadonlySet of touched field names
form.control._updateField(name, value);
form.control._touchField(name);
Localization Runtime
Wrap a subtree in LocalizationProvider. Each provider keeps its locale state
separate and can use an application-provided storage adapter with get(), set(locale), and optional remove() methods. Built-in strategies are localStorageStrategy, sessionStorageStrategy, cookieStrategy, memoryStrategy, and composeStorage.
<script setup lang="ts">
import { LocalizationProvider, localStorageStrategy } from "@valfuse-node/vue";
import manifest from "./assets/localizations/localization";
const storage = localStorageStrategy();
</script>
<template>
<LocalizationProvider :manifest="manifest" initial-locale="en" :storage="storage">
<AppContent />
</LocalizationProvider>
</template>
Call useLocalization() from a descendant's setup() or <script setup>.
Its locale is a readonly Vue computed ref; change it with setLocale().
import { computed } from "vue";
import { useLocalization } from "@valfuse-node/vue";
const { locale, setLocale, format, namespace } = useLocalization();
const welcome = computed(() => format("common.welcome", { name: "Valfuse" }));
const errors = namespace("common.errors");
setLocale("id");
errors.translate("required");
The localizer includes translate, format, plural, gender, context,
null-safe variants, namespace, entriesForLocale, and a low-level store.
useLocalizationTree() returns a computed { strings, placeholders } tree.
The browser-safe lower-level API remains available from
@valfuse-node/localization/runtime.
Custom Inputs
Use ValfuseController from @valfuse-node/vue to bind custom inputs with
Vue's v-model contract. For field-specific TypeScript inference in script
setup, use useValfuseController(form.control, "fieldName").
<template>
<ValfuseController :control="form.control" name="role" v-slot="{ field, fieldState }">
<RolePicker v-bind="field" :error="fieldState.error?.message" />
</ValfuseController>
</template>
The controller updates the same form state as register(), including validation, touched state, dirty state, and watchers.
API Parity vs React
| Feature | React (@valfuse-node/react) |
Vue (@valfuse-node/vue) |
|---|---|---|
| Field binding | {...form.register('f')} (JSX spread) |
v-bind="form.register('f')" |
| Custom field | <ValfuseController> + form.control |
<ValfuseController> or useValfuseController |
| Watch snapshot (all) | form.watch() |
form.watch() |
| Watch snapshot (one field) | form.watch("email") |
form.watch("email") |
| Watch snapshot (multi) | form.watch(["a", "b"]) |
form.watch(["a", "b"]) |
| Watch subscribe (global) | form.watch((values, info) => …) |
form.watch((values, info) => …) |
| Watch subscribe (one field) | — | form.watch("email", cb) (legacy) |
| Manual trigger | form.trigger() |
form.trigger() |
| Localization runtime | <LocalizationProvider> + useLocalization() |
<LocalizationProvider> + useLocalization() |
| Mode values | onSubmit | onChange | onBlur | onTouched | all |
onSubmit | onChange | onBlur | onTouched | all |
The adapters share schema, form lifecycle, validation, watch, and error APIs.
Their field bindings remain framework-native; Vue also exposes getValue() and
getValues() helpers.
Type Reference
import type {
UseValfuseFormProps,
UseValfuseFormReturn,
ValfuseFormMode,
ValfuseFormState,
ValfuseRegisterReturn,
} from "@valfuse-node/vue";
Development Usage
Share a schema across web (React) and mobile (Vue)
// schemas/user.ts
import { createSchema } from "@valfuse-node/form";
export const userSchema = createSchema({
name: { type: "string", rules: [{ name: "required", error: { message: "Required" } }] },
email: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "email", error: { message: "Invalid" } },
],
},
});
export type UserValues = { name: string; email: string };
<!-- Vue component -->
<script setup lang="ts">
import { useValfuseForm } from "@valfuse-node/vue";
import { userSchema, type UserValues } from "~/schemas/user";
const form = useValfuseForm<UserValues>({
schema: userSchema,
defaultValues: { name: "", email: "" },
mode: "onBlur",
});
</script>
Use a validation mode that matches UX intent
// Aggressive — validates on every keystroke
useValfuseForm({ schema, defaultValues, mode: "onChange" });
// Friendly — validates on blur (recommended for most forms)
useValfuseForm({ schema, defaultValues, mode: "onBlur" });
// Lazy — only validates on submit
useValfuseForm({ schema, defaultValues, mode: "onSubmit" });
Inject server errors
async function onSubmit(values: UserValues) {
try {
await api.saveUser(values);
} catch (err) {
form.setErrors({
email: { message: "Email is already taken", code: "auth.duplicate", type: "server" },
});
}
}
Build a reactive debug panel
<script setup lang="ts">
import { computed } from "vue";
const values = computed(() => form.getValues());
const dirty = computed(() => form.formState.dirtyFields);
</script>
<template>
<pre>values: {{ values }}</pre>
<pre>dirty: {{ dirty }}</pre>
<pre>valid: {{ form.formState.isValid }}</pre>
</template>
Release history
Release notes for this independently versioned package are maintained in the monorepo's changelog.
Support
For usage questions, bug reports, and feature requests, see the project support guide. For security reports, follow the security policy. Do not use public issues for security reports.
License
Related Packages
Umbrella entry point for Valfuse runtime APIs, framework adapters, and explicit...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.4.0 | >=3 | MIT | |
| 0.3.4 | >=3 | MIT | |
| 0.3.2 | >=3 | MIT | |
| 0.3.1 | >=3 | MIT | |
| 0.3.0 | >=3 | MIT | |
| 0.2.0 | >=3 | MIT |