@ilingo/validup-vue

Vue 3 plugin + composables that wire @ilingo/validup into a Vue app.
5,036 4
Install
npm install @ilingo/validup-vue
Latest Version:1.2.0
Vue:3
License:MIT
Last Updated:Oct 20, 2026
Links: GitHub  ·  npm
Maintainer: tada5hi

@ilingo/validup-vue

@ilingo/validup-vue

The Vue 3 plugin for @ilingo/validup: composables, renderless components, and the install hook.

npm version npm downloads minzipped size main Conventional Commits

The install hook, five composables, the <IValidup> / <IValidupT> / <IFieldValidation> renderless components, and the FieldTranslations / GroupTranslations / FieldValidation aliases.

Sibling of @ilingo/vue and @ilingo/vuelidate; mirrors the validup → @validup/vue package split so the framework-agnostic validation-message surface (@ilingo/validup) stays free of Vue.

Installation

npm install @ilingo/validup-vue @ilingo/validup @ilingo/vue ilingo validup @validup/vue vue @ebec/core

Quick start

<script setup lang="ts">
import { defineIssueItem, IssueCode } from '@ebec/core';
import { Container } from 'validup';
import { useValidup } from '@validup/vue';
import { useTranslationsForField } from '@ilingo/validup-vue';
import { reactive } from 'vue';

const formState = reactive({ email: '' });
const container = new Container<{ email: string }>();
// container.mount('email', isString) ...

const $v = useValidup(container, formState);
const emailErrors = useTranslationsForField($v.fields.email);
</script>

<template>
    <input v-model="$v.fields.email.$model" />
    <small v-for="t in emailErrors" :key="t.issue.code">{{ t.message }}</small>
</template>

Main entry plumbing (app.use order matters: @ilingo/vue first):

import { createApp } from 'vue';
import { install as installIlingo } from '@ilingo/vue';
import { install as installIlingoValidup } from '@ilingo/validup-vue';
import App from './App.vue';

const app = createApp(App);
installIlingo(app, { locale: 'en' });
installIlingoValidup(app);  // looks up the Ilingo from @ilingo/vue
app.mount('#app');

API

Vue plugin

import { install } from '@ilingo/validup-vue';

install(app);

Looks up the Ilingo instance previously installed by @ilingo/vue and registers the default Store (from @ilingo/validup) onto it. Throws a pointed error when called without a pre-installed Ilingo, which is better than silently constructing a second instance that <ITranslate> and useTranslation() wouldn't see. Idempotent: re-calling won't stack duplicate Store instances.

Composables

Export What it does
useTranslationsForIssues(issues) MaybeRefOrGetter<Issue[]> → reactive Ref<IssueTranslation[]>. Flattens to leaves and translates each.
useTranslationsForField(fieldState) MaybeRef<FieldState> from @validup/vue → reactive translations of the field's dirty-gated $errors.
useTranslationsForComposable($v) MaybeRef<Composable<T>> from @validup/vue → reactive translations of every field's $errors.
useTranslationsForGroupErrors($v) MaybeRef<Composable<T>> → reactive Ref<IssueGroupTranslation[]>. Translates $groupErrors by each group's own code without descending into children, for whole-form / banner rendering, not per-field.
useFieldValidation(fieldState) MaybeRef<FieldState> → a reactive { severity, messages, issues } bundle for one binding onto a form-group host's :validation prop.

All re-run when the injected locale flips. The injected Ilingo instance and locale Ref come from @ilingo/vue: call its install() first, then this package's install(app) to register the default catalog.

useTranslationsForIssues and useTranslationsForGroupErrors preserve the previously-resolved translations during async re-evaluation, so a locale switch on a form with visible errors doesn't blank the UI for a tick before the new translations paint. Both also seed their first value synchronously where the catalog allows it (translateIssuesSync / translateIssueGroupsSync under the hood), so the messages are present on the very first render rather than one tick later: no blink on mount, and identical markup on both sides of an SSR boundary (#988). The seed is all-or-nothing, but it only backs off when a store would need I/O. An untranslated code still seeds, from the issue's own message, exactly as the async pass would resolve it. It never shows a message that then changes.

useFieldValidation

Collapses the three reactive shims a per-field validation block usually needs (severity, translated messages, reshape) into one binding onto vuecs's <VCFormGroup :validation> prop:

<script setup lang="ts">
import { useFieldValidation } from '@ilingo/validup-vue';
const validation = useFieldValidation($v.fields.email);
</script>

<template>
    <VCFormGroup :validation="validation">
        <VCFormInput v-model="$v.fields.email.$model" />
    </VCFormGroup>
</template>

validation is a reactive bundle so its keys auto-unwrap when bound:

  • severity: getSeverity(field) from @validup/vue (undefined while pristine); the host's validation-severity.
  • messages: { key: issue.code ?? 'validation', value: message }[]; the host's validation-messages.
  • issues: the raw IssueTranslation[] escape hatch for consumers that want richer rendering.

Call it in setup(), not inline in the template. Like every composable here it wires a computedAsync watcher, owned by the effect scope active at call time: the component scope from setup() (created once, disposed on unmount), but no scope at all on the render path. Calling it inline as :validation="useFieldValidation(...)" registers a fresh, never-disposed watcher on every render and hangs the page on typing (#965). For the template-only ergonomic without a setup() line, use the <IFieldValidation> component below, which owns the lifecycle for you.

Component

<IFieldValidation>: severity and messages without a setup() line

Renderless companion to useFieldValidation for template-only use. Because it is a component, the useFieldValidation call (and its watcher) runs in the component's own setup() scope (created once, disposed on unmount), so it is the leak-free way to get the bundle straight into the template. Mirrors the <IValidup> / <IValidupT> renderless pattern. The default scoped slot exposes the bundle as value (the name already says "validation"):

<IFieldValidation :field="$v.fields.email" v-slot="{ value }">
    <VCFormGroup :validation="value">
        <VCFormInput v-model="$v.fields.email.$model" />
    </VCFormGroup>
</IFieldValidation>

Without a default slot it renders nothing.

<IValidup> leaf mode: :issues
<IValidup :issues="$v.fields.email.$errors.value">
    <template #default="{ translations }">
        <li v-for="t in translations" :key="t.issue.code">
            {{ t.message }}
        </li>
    </template>
</IValidup>

Without a default slot, renders one text node per translation. With a slot, the consumer receives the IssueTranslation[] and renders whatever structure makes sense.

<IValidup> composable mode: :composable

Pass the whole @validup/vue Composable<T> to render all three error channels it exposes, each via its own named slot:

<IValidup :composable="$v">
    <template #cross-cutting="{ translations }">
        <!-- path-less $crossCuttingErrors (CSRF, rate-limit, schema-level) -->
    </template>
    <template #groups="{ translations }">
        <!-- $groupErrors, e.g. ONE_OF_FAILED, translated by group code -->
    </template>
    <template #fields="{ translations }">
        <li v-for="t in translations" :key="t.issue.path.join('.')">{{ t.message }}</li>
    </template>
</IValidup>

Each stream renders via its slot when provided, else falls back to plain text, so with no slots the component renders all three in order (cross-cutting, groups, fields). :composable wins when both props are passed; :issues is the leaf shortcut. The #groups slot carries GroupSlotProps ({ translations: IssueGroupTranslation[] }); the others carry SlotProps.

<IValidupT>: component-aware interpolation

Slot-aware sibling of <IValidup>, built on @ilingo/vue's <ITranslateT>. Lets a validation message carry Vue components inline (<router-link>, sign-in <button>, help popover) via {slot} placeholders in the message.

<IValidupT :issues="$v.fields.password.$errors.value">
    <template #passwordField="{ issue, code }">
        <router-link :to="`#${issue.data.other}`">{{ issue.data.other }}</router-link>
    </template>
</IValidupT>
  • No placeholder slots → text path. Behaves exactly like <IValidup :issues> (text via useTranslationsForIssues, preserving the issue.message fallback for un-cataloged codes); none of the <ITranslateT> cost.
  • Placeholder slots → component path. Each issue renders through <ITranslateT path="validup.<code>" :data="issue.data">; named slots are forwarded as the {slot} fillers, each receiving an IssueSlotProps { issue, code } scope so the same slot name can render different content per issue. A code-less issue falls back to its raw message.

Per-issue element tag follows <ITranslateT>'s tag prop (default span; tag="" → fragment). The locale prop is honoured on both paths. Forward-compat: useful once messages carry placeholders. The built-in catalog messages are all plain text today.

Note on the component path: because it renders through <ITranslateT>, an un-cataloged code resolves to the literal "validup.<code>" (not issue.message); <ITranslateT> has no issue.message fallback. This is the intended usage boundary: you reach for slot mode precisely for messages that do have a catalog entry with placeholders. The text path keeps the issue.message fallback. Groups are also not flattened on the component path: pass leaf $errors, not raw $issues.

FieldTranslations / GroupTranslations / FieldValidation

FieldTranslations is Ref<IssueTranslation[]> (leaf composables); GroupTranslations is Ref<IssueGroupTranslation[]> (useTranslationsForGroupErrors); FieldValidation is the reactive { severity, messages, issues } shape (useFieldValidation) consumed by vuecs's <VCFormGroup :validation>. Re-exported so consumers can type props without reaching into Vue's Ref directly.

License

MIT © Peter Placzek

Related Packages

@ilingo/vuelidate

Vuelidate message adapter for ilingo — drop-in localized validator messages (EN/...

10,433 4
@dialpad/i18n

Internationalization library for Vue 3 apps using Mozilla's Fluent syntax, with...

10,073
@ilingo/vue

Vue 3 integration for ilingo — provide/inject, reactive locale, the component, a...

12,677 4
vue-simple-i18n

Probably the thinnest library to end all Vue i18n library within 1kb

470 33
@mylukin/easy-i18n-js

Framework-agnostic i18n extraction tool with plugin support for Svelte, Vue, and...

5,836 0

Version History

Version Released Vue License
1.2.0 ^3.5.41 MIT
1.1.0 ^3.5.40 MIT
1.0.1 ^3.5.35 MIT
1.0.0 ^3.5.35 MIT