Forms and validation
Use useAbpForm to keep values, interaction state and validation together. A form is a flat set of controls; the current implementation does not provide nested form groups or dynamic arrays.
Submit a real request
Create src/pages/ProductForm.vue from the following example. It assumes POST /api/app/product accepts { name: string }; adapt that endpoint and DTO to your service. The application must already register core and a theme as described in startup.
<template>
<form @submit.prevent="save">
<AbpFormField
v-slot="{ id, describedBy, invalid }"
:label="localization.t('AbpIdentity::DisplayName:Name')"
:errors="messages"
required
>
<AbpInput
:id="id"
v-model="form.controls.name.value"
:aria-describedby="describedBy"
:invalid="invalid"
:disabled="saving"
/>
</AbpFormField>
<p v-for="message in form.unmatchedServerErrors" :key="message" role="alert">
{{ message }}
</p>
<AbpButton type="submit" :loading="saving">{{ localization.t('AbpUi::Save') }}</AbpButton>
</form>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { inject, RestService, useLocalization } from '@lsw-abpvue/core';
import {
AbpButton,
AbpFormField,
AbpInput,
useAbpForm,
useServerValidation,
useValidationMessages,
Validators,
} from '@lsw-abpvue/theme-shared';
const rest = inject(RestService);
const localization = useLocalization();
const form = useAbpForm({
name: { value: '', validators: [Validators.required(), Validators.maxLength(128)] },
});
useServerValidation(form);
const validationMessages = useValidationMessages();
const messages = computed(() =>
form.controls.name.touched ? validationMessages(form.controls.name.errors) : [],
);
const saving = ref(false);
async function save(): Promise<void> {
if (saving.value || !form.validate()) return;
saving.value = true;
try {
await rest.request<{ name: string }, unknown>({
method: 'POST',
url: '/api/app/product',
body: form.value,
});
form.reset();
} catch {
// Framework handlers report errors; keep the entered values for correction.
} finally {
saving.value = false;
}
}
</script>Try an empty submission, then a valid name. The handler marks every field touched, validates, prevents repeated submission, and only resets after success. A rejected request retains values.
Values and interaction state
form.controls.name.value is the value directly, not another ref. form.value returns the current object. Do not destructure a primitive control value into a separate variable and expect it to stay reactive.
| Operation | Appropriate use |
|---|---|
Write control.value | User edit; marks dirty and drops old server errors |
form.patch(values) | Load values without marking dirty |
form.reset(values) | Start a new editing baseline and clear touched/dirty/server errors |
control.markAsTouched() | Show errors after blur |
form.validate() | Mark all touched and return whether submission is valid |
form.clearServerErrors() | Remove previous backend errors without changing values |
patch does not clear an existing dirty state. Use reset when opening a different record. For edit forms, retain fields absent from the UI, extra properties and the concurrency stamp when building the update DTO.
Built-in rules
| Rule | Example |
|---|---|
| Required | Validators.required() |
| Length | Validators.minLength(4), Validators.maxLength(128) |
| Numeric range | Validators.min(1), Validators.max(20), Validators.range(1, 20) |
| Format | Validators.email(), Validators.url(), Validators.pattern(/\d{4}/) |
| Match another field | Validators.compare('password') |
Optional empty values pass rules other than required. Required accepts false as a defined boolean, so accepting terms needs a rule requiring true. HTML min/max/maxlength are useful input constraints but do not replace validators. For a backend-generated DTO, reuse its generated validator map and explicitly compose inherited rules; it covers supported annotations, not arbitrary business logic.
Custom and conditional rules
Create src/forms/registration.ts with these synchronous validators:
import { useAbpForm, Validators, type AbpValidator } from '@lsw-abpvue/theme-shared';
export const acceptTerms: AbpValidator<boolean> = value =>
value === true
? null
: {
rule: 'acceptTerms',
key: { key: 'BookStore::AcceptTerms', defaultValue: 'Accept the terms before continuing.' },
params: [],
};
export const companyWhenBusiness: AbpValidator<string> = (value, context) => {
if (context.valueOf('business') !== true || value.trim()) return null;
return {
rule: 'company',
key: {
key: 'BookStore::CompanyRequired',
defaultValue: 'Company is required for a business account.',
},
params: [],
};
};
export function createRegistrationForm() {
return useAbpForm({
business: { value: false },
company: { value: '', validators: [companyWhenBusiness, Validators.maxLength(128)] },
accepted: { value: false, validators: [acceptTerms] },
password: { value: '', validators: [Validators.required(), Validators.minLength(8)] },
confirmation: { value: '', validators: [Validators.compare('password')] },
});
}A validator returns null or { rule, key, params }. The context reads another control through valueOf. In the example, Company is required only for a business account and confirmation must match password. Bind the company control's visibility in the page separately; hiding a field does not automatically disable its validator.
Validators do not return Promises. For a remote uniqueness check, run a separate cancellable request and show its status, then let the backend enforce uniqueness on submission. Do not use a successful earlier lookup as authorization to save.
Localized messages
Call useValidationMessages() once in setup, then convert errors inside computed values. This keeps their text reactive when culture changes. Every built-in validator accepts a final localization-message argument. Pass a resource key or a key with fallback text to override it. The field example shows a custom required/email message and the touched-only display policy.
Backend validation
Call useServerValidation(form) during setup before sending requests. It connects the form to framework validation handling. Backend members are matched case-insensitively and by the last path segment, so Name and ExtraProperties.Name can reach a name control. Display unmatchedServerErrors near the submit action; an error without a matching field must remain visible.
The framework's HTTP handlers report failures. A local catch can preserve the form without adding a duplicate generic error. If you disable global handling for a request, you must display its failure yourself. See HTTP errors.
Editing in a dialog
Load/reset before opening, bind form.dirty and saving to AbpModal, and call the footer's guarded close for Cancel. Keep the dialog open on failure. See modal forms.
Check required, conditional, cross-field and backend errors, double submission, failed save, reset, and a language change. These are distinct behaviors; a green type check does not exercise a business endpoint.