Agent skill
umbraco-validation-context
Implement form validation using UmbValidationContext in Umbraco backoffice
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/umbraco-validation-context
SKILL.md
Umbraco Validation Context
What is it?
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.
Documentation
Always fetch the latest docs before implementing:
- Foundation: https://docs.umbraco.com/umbraco-cms/customizing/foundation
- Extension Registry: https://docs.umbraco.com/umbraco-cms/customizing/extending-overview/extension-registry
Reference Examples
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.
Related Foundation Skills
-
State Management: For observing validation state changes
- Reference skill:
umbraco-state-management
- Reference skill:
-
Context API: For consuming validation context
- Reference skill:
umbraco-context-api
- Reference skill:
Workflow
- Fetch docs - Use WebFetch on the URLs above
- Ask questions - What fields? What validation rules? Multi-tab form?
- Generate files - Create form element with validation context
- Explain - Show what was created and how validation works
Basic Setup
import { 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!');
}
}
}
Multi-Tab Form with Error Counting
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');
}
}
}
Server-Side 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);
}
}
Key APIs
UmbValidationContext
// Create context
const validation = new UmbValidationContext(this);
// Validate all bound fields
const isValid = await validation.validate();
// Access messages manager
validation.messages;
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 */ }
);
umbBindToValidation Directive
// Bind an input to validation
${umbBindToValidation(this, '$.form.fieldName', fieldValue)}
JSON Path Notation
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 |
Validation Message Interface
interface UmbValidationMessage {
source: string; // 'client' | 'server' | custom
path: string; // JSON Path
message: string; // Error message text
key: string; // Unique identifier
}
Best Practices
- Use JSON Path hierarchy - Organize paths by tab/section for easy error counting
- Wrap inputs - Use
<uui-form-validation-message>around inputs - Clear server errors - Remove old server errors before new validation
- Unique keys - Use
crypto.randomUUID()for server error keys - Observe specific paths - Use
messagesOfPathAndDescendantfor scoped error counts - Show counts on tabs - Display error badges to guide users to problems
That's it! Always fetch fresh docs, keep examples minimal, generate complete working code.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?