umbraco-validation-context by umbraco/umbraco-cms-backoffice-skills
npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-validation-contextUmbValidationContext 为 Umbraco 后台中的表单提供了一个集中式的验证系统。它使用 JSON Path 符号管理验证消息,支持客户端和服务器端验证,并能为选项卡和区域启用响应式错误计数。这对于多步骤表单、工作区编辑器以及任何需要全面验证反馈的用户界面至关重要。
在实施前请务必获取最新文档:
Umbraco 源代码包含工作示例:
验证上下文仪表板 : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/validation-context/
此示例演示了带错误计数的多选项卡表单验证。
自定义验证工作区上下文 : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/custom-validation-workspace-context/
此示例展示了工作区级别的验证模式。
: 用于观察验证状态变化
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
umbraco-state-management上下文 API : 用于使用验证上下文
umbraco-context-apiimport { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import {
UMB_VALIDATION_CONTEXT,
umbBindToValidation,
UmbValidationContext,
} from '@umbraco-cms/backoffice/validation';
import type { UmbValidationMessage } from '@umbraco-cms/backoffice/validation';
@customElement('my-validated-form')
export class MyValidatedFormElement extends UmbLitElement {
// 为此组件创建验证上下文
readonly validation = new UmbValidationContext(this);
@state()
private _name = '';
@state()
private _email = '';
@state()
private _messages?: UmbValidationMessage[];
constructor() {
super();
// 观察所有验证消息
this.consumeContext(UMB_VALIDATION_CONTEXT, (validationContext) => {
this.observe(
validationContext?.messages.messages,
(messages) => {
this._messages = messages;
},
'observeValidationMessages'
);
});
}
override render() {
return html`
<uui-form>
<form>
<div>
<label>姓名</label>
<uui-form-validation-message>
<uui-input
type="text"
.value=${this._name}
@input=${(e: InputEvent) => (this._name = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.name', this._name)}
required
></uui-input>
</uui-form-validation-message>
</div>
<div>
<label>邮箱</label>
<uui-form-validation-message>
<uui-input
type="email"
.value=${this._email}
@input=${(e: InputEvent) => (this._email = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.email', this._email)}
required
></uui-input>
</uui-form-validation-message>
</div>
<uui-button look="primary" @click=${this.#handleSave}>保存</uui-button>
</form>
</uui-form>
<pre>${JSON.stringify(this._messages ?? [], null, 2)}</pre>
`;
}
async #handleSave() {
const isValid = await this.validation.validate();
if (isValid) {
// 表单有效,继续保存
console.log('表单有效!');
}
}
}
import { html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbValidationContext, umbBindToValidation } from '@umbraco-cms/backoffice/validation';
@customElement('my-tabbed-form')
export class MyTabbedFormElement extends UmbLitElement {
readonly validation = new UmbValidationContext(this);
@state() private _tab = '1';
@state() private _totalErrors = 0;
@state() private _tab1Errors = 0;
@state() private _tab2Errors = 0;
// 表单字段
@state() private _name = '';
@state() private _email = '';
@state() private _city = '';
@state() private _country = '';
constructor() {
super();
// 观察总错误数
this.observe(
this.validation.messages.messagesOfPathAndDescendant('$.form'),
(messages) => {
this._totalErrors = [...new Set(messages.map((x) => x.path))].length;
}
);
// 观察选项卡 1 的错误(使用 JSON Path 前缀)
this.observe(
this.validation.messages.messagesOfPathAndDescendant('$.form.tab1'),
(messages) => {
this._tab1Errors = [...new Set(messages.map((x) => x.path))].length;
}
);
// 观察选项卡 2 的错误
this.observe(
this.validation.messages.messagesOfPathAndDescendant('$.form.tab2'),
(messages) => {
this._tab2Errors = [...new Set(messages.map((x) => x.path))].length;
}
);
}
override render() {
return html`
<uui-box>
<p>总错误数:${this._totalErrors}</p>
<uui-tab-group @click=${this.#onTabChange}>
<uui-tab ?active=${this._tab === '1'} data-tab="1">
选项卡 1
${when(
this._tab1Errors,
() => html`<uui-badge color="danger">${this._tab1Errors}</uui-badge>`
)}
</uui-tab>
<uui-tab ?active=${this._tab === '2'} data-tab="2">
选项卡 2
${when(
this._tab2Errors,
() => html`<uui-badge color="danger">${this._tab2Errors}</uui-badge>`
)}
</uui-tab>
</uui-tab-group>
${when(this._tab === '1', () => this.#renderTab1())}
${when(this._tab === '2', () => this.#renderTab2())}
<uui-button look="primary" @click=${this.#handleSave}>保存</uui-button>
</uui-box>
`;
}
#renderTab1() {
return html`
<uui-form>
<form>
<label>姓名</label>
<uui-form-validation-message>
<uui-input
.value=${this._name}
@input=${(e: InputEvent) => (this._name = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.tab1.name', this._name)}
required
></uui-input>
</uui-form-validation-message>
<label>邮箱</label>
<uui-form-validation-message>
<uui-input
type="email"
.value=${this._email}
@input=${(e: InputEvent) => (this._email = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.tab1.email', this._email)}
required
></uui-input>
</uui-form-validation-message>
</form>
</uui-form>
`;
}
#renderTab2() {
return html`
<uui-form>
<form>
<label>城市</label>
<uui-form-validation-message>
<uui-input
.value=${this._city}
@input=${(e: InputEvent) => (this._city = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.tab2.city', this._city)}
required
></uui-input>
</uui-form-validation-message>
<label>国家</label>
<uui-form-validation-message>
<uui-input
.value=${this._country}
@input=${(e: InputEvent) => (this._country = (e.target as HTMLInputElement).value)}
required
></uui-input>
</uui-form-validation-message>
</form>
</uui-form>
`;
}
#onTabChange(e: Event) {
this._tab = (e.target as HTMLElement).getAttribute('data-tab') ?? '1';
}
async #handleSave() {
const isValid = await this.validation.validate();
if (!isValid) {
console.log('表单存在验证错误');
}
}
}
在 API 调用后添加服务器验证错误:
async #handleSave() {
// 首先进行客户端验证
const isValid = await this.validation.validate();
if (!isValid) return;
try {
// 调用 API
const response = await this.#saveToServer();
if (!response.ok) {
// 添加服务器验证错误
const errors = await response.json();
for (const error of errors.validationErrors) {
this.validation.messages.addMessage(
'server', // 来源
error.path, // JSON Path(例如 '$.form.name')
error.message, // 错误消息
crypto.randomUUID() // 唯一键
);
}
}
} catch (error) {
console.error('保存失败:', error);
}
}
// 创建上下文
const validation = new UmbValidationContext(this);
// 验证所有绑定的字段
const isValid = await validation.validate();
// 访问消息管理器
validation.messages;
// 添加消息
validation.messages.addMessage(source, path, message, key);
// 按来源移除消息
validation.messages.removeMessagesBySource('server');
// 观察某个路径及其子路径的消息
this.observe(
validation.messages.messagesOfPathAndDescendant('$.form.tab1'),
(messages) => { /* 处理消息 */ }
);
// 观察所有消息
this.observe(
validation.messages.messages,
(messages) => { /* 处理所有消息 */ }
);
// 将输入绑定到验证
${umbBindToValidation(this, '$.form.fieldName', fieldValue)}
验证使用 JSON Path 来标识字段:
| 路径 | 描述 |
|---|---|
$.form | 根表单对象 |
$.form.name | 姓名字段 |
$.form.tab1.email | tab1 中的邮箱字段 |
$.form.items[0].value | 第一个项目的值 |
$.form.items[*].name | 所有项目的名称 |
interface UmbValidationMessage {
source: string; // 'client' | 'server' | 自定义
path: string; // JSON Path
message: string; // 错误消息文本
key: string; // 唯一标识符
}
<uui-form-validation-message>crypto.randomUUID() 作为服务器错误键messagesOfPathAndDescendant 获取作用域内的错误计数就是这样!务必获取最新文档,保持示例简洁,生成完整的工作代码。
每周安装数
68
仓库
GitHub 星标数
14
首次出现
2026年2月4日
安全审计
安装于
github-copilot49
cursor22
opencode20
codex20
gemini-cli18
amp18
UmbValidationContext provides a centralized validation system for forms in the Umbraco backoffice. It manages validation messages using JSON Path notation, supports both client-side and server-side validation, and enables reactive error counting for tabs and sections. This is essential for multi-step forms, workspace editors, and any UI that requires comprehensive validation feedback.
Always fetch the latest docs before implementing:
The Umbraco source includes working examples:
Validation Context Dashboard : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/validation-context/
This example demonstrates multi-tab form validation with error counting.
Custom Validation Workspace Context : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/custom-validation-workspace-context/
This example shows workspace-level validation patterns.
State Management : For observing validation state changes
umbraco-state-managementContext API : For consuming validation context
umbraco-context-apiimport { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import {
UMB_VALIDATION_CONTEXT,
umbBindToValidation,
UmbValidationContext,
} from '@umbraco-cms/backoffice/validation';
import type { UmbValidationMessage } from '@umbraco-cms/backoffice/validation';
@customElement('my-validated-form')
export class MyValidatedFormElement extends UmbLitElement {
// Create validation context for this component
readonly validation = new UmbValidationContext(this);
@state()
private _name = '';
@state()
private _email = '';
@state()
private _messages?: UmbValidationMessage[];
constructor() {
super();
// Observe all validation messages
this.consumeContext(UMB_VALIDATION_CONTEXT, (validationContext) => {
this.observe(
validationContext?.messages.messages,
(messages) => {
this._messages = messages;
},
'observeValidationMessages'
);
});
}
override render() {
return html`
<uui-form>
<form>
<div>
<label>Name</label>
<uui-form-validation-message>
<uui-input
type="text"
.value=${this._name}
@input=${(e: InputEvent) => (this._name = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.name', this._name)}
required
></uui-input>
</uui-form-validation-message>
</div>
<div>
<label>Email</label>
<uui-form-validation-message>
<uui-input
type="email"
.value=${this._email}
@input=${(e: InputEvent) => (this._email = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.email', this._email)}
required
></uui-input>
</uui-form-validation-message>
</div>
<uui-button look="primary" @click=${this.#handleSave}>Save</uui-button>
</form>
</uui-form>
<pre>${JSON.stringify(this._messages ?? [], null, 2)}</pre>
`;
}
async #handleSave() {
const isValid = await this.validation.validate();
if (isValid) {
// Form is valid, proceed with save
console.log('Form is valid!');
}
}
}
import { html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbValidationContext, umbBindToValidation } from '@umbraco-cms/backoffice/validation';
@customElement('my-tabbed-form')
export class MyTabbedFormElement extends UmbLitElement {
readonly validation = new UmbValidationContext(this);
@state() private _tab = '1';
@state() private _totalErrors = 0;
@state() private _tab1Errors = 0;
@state() private _tab2Errors = 0;
// Form fields
@state() private _name = '';
@state() private _email = '';
@state() private _city = '';
@state() private _country = '';
constructor() {
super();
// Observe total errors
this.observe(
this.validation.messages.messagesOfPathAndDescendant('$.form'),
(messages) => {
this._totalErrors = [...new Set(messages.map((x) => x.path))].length;
}
);
// Observe Tab 1 errors (using JSON Path prefix)
this.observe(
this.validation.messages.messagesOfPathAndDescendant('$.form.tab1'),
(messages) => {
this._tab1Errors = [...new Set(messages.map((x) => x.path))].length;
}
);
// Observe Tab 2 errors
this.observe(
this.validation.messages.messagesOfPathAndDescendant('$.form.tab2'),
(messages) => {
this._tab2Errors = [...new Set(messages.map((x) => x.path))].length;
}
);
}
override render() {
return html`
<uui-box>
<p>Total errors: ${this._totalErrors}</p>
<uui-tab-group @click=${this.#onTabChange}>
<uui-tab ?active=${this._tab === '1'} data-tab="1">
Tab 1
${when(
this._tab1Errors,
() => html`<uui-badge color="danger">${this._tab1Errors}</uui-badge>`
)}
</uui-tab>
<uui-tab ?active=${this._tab === '2'} data-tab="2">
Tab 2
${when(
this._tab2Errors,
() => html`<uui-badge color="danger">${this._tab2Errors}</uui-badge>`
)}
</uui-tab>
</uui-tab-group>
${when(this._tab === '1', () => this.#renderTab1())}
${when(this._tab === '2', () => this.#renderTab2())}
<uui-button look="primary" @click=${this.#handleSave}>Save</uui-button>
</uui-box>
`;
}
#renderTab1() {
return html`
<uui-form>
<form>
<label>Name</label>
<uui-form-validation-message>
<uui-input
.value=${this._name}
@input=${(e: InputEvent) => (this._name = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.tab1.name', this._name)}
required
></uui-input>
</uui-form-validation-message>
<label>Email</label>
<uui-form-validation-message>
<uui-input
type="email"
.value=${this._email}
@input=${(e: InputEvent) => (this._email = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.tab1.email', this._email)}
required
></uui-input>
</uui-form-validation-message>
</form>
</uui-form>
`;
}
#renderTab2() {
return html`
<uui-form>
<form>
<label>City</label>
<uui-form-validation-message>
<uui-input
.value=${this._city}
@input=${(e: InputEvent) => (this._city = (e.target as HTMLInputElement).value)}
${umbBindToValidation(this, '$.form.tab2.city', this._city)}
required
></uui-input>
</uui-form-validation-message>
<label>Country</label>
<uui-form-validation-message>
<uui-input
.value=${this._country}
@input=${(e: InputEvent) => (this._country = (e.target as HTMLInputElement).value)}
required
></uui-input>
</uui-form-validation-message>
</form>
</uui-form>
`;
}
#onTabChange(e: Event) {
this._tab = (e.target as HTMLElement).getAttribute('data-tab') ?? '1';
}
async #handleSave() {
const isValid = await this.validation.validate();
if (!isValid) {
console.log('Form has validation errors');
}
}
}
Add server validation errors after an API call:
async #handleSave() {
// First validate client-side
const isValid = await this.validation.validate();
if (!isValid) return;
try {
// Call API
const response = await this.#saveToServer();
if (!response.ok) {
// Add server validation errors
const errors = await response.json();
for (const error of errors.validationErrors) {
this.validation.messages.addMessage(
'server', // Source
error.path, // JSON Path (e.g., '$.form.name')
error.message, // Error message
crypto.randomUUID() // Unique key
);
}
}
} catch (error) {
console.error('Save failed:', error);
}
}
// Create context
const validation = new UmbValidationContext(this);
// Validate all bound fields
const isValid = await validation.validate();
// Access messages manager
validation.messages;
// Add a message
validation.messages.addMessage(source, path, message, key);
// Remove messages by source
validation.messages.removeMessagesBySource('server');
// Observe messages for a path and descendants
this.observe(
validation.messages.messagesOfPathAndDescendant('$.form.tab1'),
(messages) => { /* handle messages */ }
);
// Observe all messages
this.observe(
validation.messages.messages,
(messages) => { /* handle all messages */ }
);
// Bind an input to validation
${umbBindToValidation(this, '$.form.fieldName', fieldValue)}
Validation uses JSON Path to identify fields:
| Path | Description |
|---|---|
$.form | Root form object |
$.form.name | Name field |
$.form.tab1.email | Email field in tab1 |
$.form.items[0].value | First item's value |
$.form.items[*].name | All item names |
interface UmbValidationMessage {
source: string; // 'client' | 'server' | custom
path: string; // JSON Path
message: string; // Error message text
key: string; // Unique identifier
}
<uui-form-validation-message> around inputscrypto.randomUUID() for server error keysmessagesOfPathAndDescendant for scoped error countsThat's it! Always fetch fresh docs, keep examples minimal, generate complete working code.
Weekly Installs
68
Repository
GitHub Stars
14
First Seen
Feb 4, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot49
cursor22
opencode20
codex20
gemini-cli18
amp18
2025 Node.js 最佳实践指南:框架选择、架构原则与错误处理
5,100 周安装
Tavily API 使用指南:网络搜索、内容提取与站点地图生成工具
261 周安装
Linear项目管理AI助手:集成MCP服务器实现自然语言问题跟踪与团队协作自动化
263 周安装
Bash Linux 命令速查与脚本模板 - 提升开发效率的终极指南
264 周安装
PostHog Web测试环境设置指南:解决Python版本冲突与依赖安装
81 周安装
GrepAI PostgreSQL存储技能:pgvector扩展实现团队代码搜索与向量数据库集成
264 周安装
Google Workspace CLI 快速安装指南:gws-install 技能配置与身份验证
275 周安装