@valfuse-node/vue

Vue adapter for valfuse-node — form, controller, and localization composables
1,137
Install
npm install @valfuse-node/vue
Latest Version:0.4.0
Vue:3
License:MIT
Last Updated:Oct 17, 2026
Links: GitHub  ·  npm
Maintainer: alpinnz

@valfuse-node/vue

Vue 3 adapter for @valfuse-node — useValfuseForm composable with reactive formState, 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

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

  1. Calls e.preventDefault() on the submit event
  2. Validates the form
  3. If valid → calls onValid(values) and sets isSubmitting = true for the duration
  4. If invalid → sets formState.errors and 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 legacy watch(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

MIT

Related Packages

@vue-formily/formily

Simple, lightweight, and flexible schema-based form for Vue.js

3,654 29
@adbros/vue-validation

Composable for schema-based form validation in Vue 3 using Valibot

6,036 0
vue-formor

Form validation for Vue in composition functions with Valibot.

1,556 5
@devlsh/form

Lightweight, type-safe forms for Vue.

172 25
@valfuse-node/core

Umbrella entry point for Valfuse runtime APIs, framework adapters, and explicit...

2,103 0

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