mcpField
U
umbraco-validation-context

umbraco-validation-context

github.com/umbraco开发27373 次安装6 周前更新
代码仓库

关于 umbraco-validation-context

面向 Umbraco 后台表单开发者的验证技能,介绍 UmbValidationContext 和基于 JSON Path 的消息管理。涵盖客户端与服务端错误,以及多标签表单的响应式错误计数。

主要功能

  1. 01使用 UmbValidationContext 集中管理验证消息
  2. 02通过 JSON Path 绑定字段
  3. 03处理客户端和服务端错误
  4. 04按路径及其后代观察错误
  5. 05在标签页和区块显示错误数

适用场景

  1. 01验证多步骤表单中的字段
  2. 02显示 API 返回的错误
  3. 03引导用户定位有问题的标签页或区块

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-validation-context

安装后在 Claude Code 中输入/umbraco-validation-context确认已加载。

详细介绍

Umbraco 验证上下文

这是什么?

UmbValidationContext 为 Umbraco backoffice 中的表单提供了集中式验证系统。它使用 JSON Path 表示法管理验证消息,支持客户端和服务端验证,并可为选项卡和分区提供响应式错误计数。这对于多步骤表单、工作区编辑器,以及任何需要全面验证反馈的 UI 都至关重要。

文档

实现前始终获取最新文档:

参考示例

Umbraco 源代码包含可运行的示例:

验证上下文仪表板:/Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/validation-context/

此示例演示了带错误计数的多选项卡表单验证。

自定义验证工作区上下文:/Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/custom-validation-workspace-context/

此示例展示了工作区级别的验证模式。

相关基础功能技能

  • 状态管理:用于观察验证状态变化

    • 参考技能:umbraco-state-management
  • 上下文 API:用于使用验证上下文

    • 参考技能:umbraco-context-api

工作流

  1. 获取文档——对以上 URL 使用 WebFetch
  2. 提出问题——有哪些字段?验证规则是什么?是多选项卡表单吗?
  3. 生成文件——创建包含验证上下文的表单元素
  4. 说明——展示创建的内容以及验证的工作方式

基本设置

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!');
    }
  }
}

带错误计数的多选项卡表单

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');
    }
  }
}

服务端验证错误

API 调用后添加服务端验证错误:

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);
  }
}

关键 API

UmbValidationContext
// 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 */ }
);
umbBindToValidation 指令
// Bind an input to validation
${umbBindToValidation(this, '$.form.fieldName', fieldValue)}

JSON Path 表示法

验证使用 JSON Path 来标识字段:

路径说明
$.form根表单对象
$.form.name名称字段
$.form.tab1.emailtab1 中的电子邮件字段
$.form.items[0].value第一项的值
$.form.items[*].name所有项目的名称

验证消息接口

interface UmbValidationMessage {
  source: string;    // 'client' | 'server' | custom
  path: string;      // JSON Path
  message: string;   // Error message text
  key: string;       // Unique identifier
}

最佳实践

  1. 使用 JSON Path 层级结构——按选项卡/分区组织路径,便于统计错误
  2. 包装输入框——在输入框外使用 <uui-form-validation-message>
  3. 清除服务端错误——在新一轮验证前移除旧的服务端错误
  4. 使用唯一键——使用 crypto.randomUUID() 生成服务端错误键
  5. 观察特定路径——使用 messagesOfPathAndDescendant 统计指定范围内的错误
  6. 在选项卡上显示计数——显示错误徽标,引导用户找到问题

就是这样!始终获取最新文档,保持示例精简,并生成完整可运行的代码。

评论

支持 Markdown