跳转到内容

核心概念:从 Schema 到 View

💡 本文介绍的是两种使用模式共享的底层解析链。具体到「如何渲染」,分为自动模式(<piying-view> 全自动)与手动模式(convertToField + 指令手动绑定),边界见 两种使用模式

本文介绍 Piying-View 的核心工作流程,帮助你理解数据如何从 Valibot Schema 转换为 UI 视图。

Schema → Field → Component 三阶段解析链

Section titled “Schema → Field → Component 三阶段解析链”

Piying-View 的运行时流程可抽象为三个阶段的流水线:

Valibot Schema ──(解析)──► Field Config ──(渲染)──► Component Tree
  1. Schema 定义:开发者使用 Valibot API 构建结构化的类型定义
  2. Field 解析:Piying-View 遍历 Schema,将每个节点解析为包含组件属性、事件监听、包装器链、子级字段等配置的 Field 配置对象
  3. Component 渲染:根据 Field 配置动态实例化对应的 UI 组件,组装为完整的组件树

Piying-View 将表单的运行时行为拆分为两个正交维度:

维度 职责
结构层(Schema) 定义字段类型、默认值、验证规则、组件映射、包装器配置等静态元数据
渲染层(Component) 根据解析后的 Field 配置,动态渲染对应的 UI 组件树

两个维度通过统一的输入/输出契约(Attributes / Events / Inputs / Outputs)进行通信。这意味着只要替换实现层的组件,Schema 定义无需任何修改——这正是跨框架兼容的基础。


Valibot Schema (v.object / v.pipe)


CoreSchemaHandle(核心处理句柄)
    │  - 解析所有 Actions(setComponent、inputs、outputs 等)
    │  - 收集元信息(priority、keyPath、alias 等)
    │  - 标记是否为非字段控制(nonFieldControl / NonFieldControlAction)

FormBuilder(构建器)
    │  - buildRoot() / buildControl() / #buildField()
    │  - 遍历所有 CoreSchemaHandle,创建 PiResolvedViewFieldConfig
    │  - 有表单控制 → 创建 FieldControl(关联表单控件)
    │  - nonFieldControl → 不创建 Form Control,仅作为视图组件渲染(keyPath 依然存在,可被查询)
    │  - 递归处理 group / array / logicGroup 的子字段
    │  - 管理注入器、销毁生命周期、作用域映射

Resolved Field Tree(已解析字段树)
    │  - FieldControl:单个值控件,关联 Form Control,持有 value/errors/validators
    │  - FieldGroup:对象控件组,包含多个子控件的 record
    │  - FieldArray:数组控件,支持增删改操作
    │  - FieldLogicGroup:intersect/union 逻辑分组
    │  - 普通组件:无表单关联,仅作为视图组件渲染(通过 setComponent 定义)

View Component(视图组件)
    │  - Angular: PiyingView / 自定义组件
    │  - Vue / React / Solid / Svelte

UI 渲染

Piying-View 基于 Valibot。Schema 定义了:

  • 字段结构:哪些字段、嵌套关系
  • 验证规则:必填、长度、范围等
  • 转换逻辑:transform / transformAsync
  • 视图配置:通过 v.pipe() 链式附加 Actions
import * as v from 'valibot';

const schema = v.object({
  name: v.pipe(
    v.string(), // 类型定义
    v.minLength(2), // 验证规则
    setComponent('my-input'), // 视图配置:使用哪个组件渲染
    formConfig({ required: true }), // 表单配置
  ),
});

2. CoreSchemaHandle(核心处理句柄)

Section titled “2. CoreSchemaHandle(核心处理句柄)”

Schema 被传入 convertToField() 函数后,Piying-View 遍历 Schema 树,为每个节点创建 CoreSchemaHandle。此句柄负责:

  • 解析 Actions:将 setComponentinputsoutputsattributeswrappers 等 Action 转换为内部数据
  • 收集元信息:优先级(priority)、路径(keyPath/fullPath)、别名(alias)
  • 应用 Hook:处理 mergeHookspatchHooks 等生命周期回调
import { convertToField } from '@piying/view-angular';

const field = convertToField(() => schema, injector, () => ({
  fieldGlobalConfig: { types, wrappers },
  context: myContext,
}));

每个 v.pipe() 中的 Action 按顺序执行:

v.pipe(
  v.string(),
  actions.inputs.set({ placeholder: '请输入' }), // 1. 设置输入属性
  setComponent('my-input'), // 2. 设置组件类型
  actions.outputs.set({ change: handleChange }), // 3. 设置输出事件
);
// → CoreSchemaHandle: {
//     type: 'my-input',
//     inputs: { placeholder: '请输入' },
//     outputs: { change: handleChange },
//     attributes: {},
//     wrappers: []
//   }

CoreSchemaHandle 通过 nonFieldControl 标记区分两种用途:

  • 有表单控制的字段(默认):有关键路径 keyPath,会创建 FieldControl/FieldGroup/FieldArray,参与表单值管理和验证
  • 无表单控制的字段nonFieldControl = true 或通过 NonFieldControlAction 定义):同样拥有关键路径 keyPath,同样可以用 field.get() 查询定位,只是 FormBuilder 不会为其创建 Form Control,不参与表单值绑定与验证
// 示例:一个不关联表单的纯展示组件
v.pipe(
  v.string(),
  setComponent('user-avatar'), // 视图组件
  nonFieldControl(), // 标记为非表单字段,不生成 Form Control
);

3. FormBuilder(构建器)— Schema Handle → Resolved Field

Section titled “3. FormBuilder(构建器)— Schema Handle → Resolved Field”

CoreSchemaHandle 本身只是原始数据结构,需要通过 FormBuilder 转换为 PiResolvedViewFieldConfig(已解析字段)。

FormBuilder 是整个转换流程的核心引擎:

  • buildRoot():从根节点开始构建
  • buildControl():遍历每个 CoreSchemaHandle,执行以下逻辑:
    1. 查找组件定义(通过 setComponent 注册的 type)
    2. 创建注入器(为字段提供 DI 能力)
    3. 如果有关键路径 → 调用 createField() 创建 Form Control(FieldControl/FieldGroup/FieldArray
    4. 构建 PiResolvedViewFieldConfig 对象,包含:inputs、outputs、attributes、slots、wrappers 等
    5. 如果 nonFieldControl = true → 跳过 Form Control 创建,仅保留组件渲染配置
  • #buildField():递归处理 group/array/logicGroup 的子字段
  • 作用域管理:通过 PI_FORM_BUILDER_ALIAS_MAP 管理字段别名映射
  • 生命周期管理:注入器销毁、allFieldsResolved hook 批量调用
// convertToField() 函数内部的调用链
const injector = Injector.create({
  providers: [
    { provide: PI_FORM_BUILDER_OPTIONS_TOKEN, useValue: buildOptions },
    builder, // FormBuilder 实例
  ],
});

convertCore(obj, (item) => {
  injector.get(builder).buildRoot({ field: item });
  return buildOptions.resolvedField$();
});
//                          │
//                          ▼
//           buildRoot → buildControl → #buildField(递归)

4. Field Control Tree & Resolved Field(字段控制树与已解析字段)

Section titled “4. Field Control Tree & Resolved Field(字段控制树与已解析字段)”

FormBuilder 输出的是 PiResolvedViewFieldConfig,它包含两类产物:

类型 说明
FieldControl 单值控件,关联 Form Control,持有 value/errors/validators
FieldGroup 对象控件组,包含多个子控件的 record
FieldArray 数组控件,支持增删改操作
FieldLogicGroup intersect/union 逻辑分组
普通组件 无表单关联,仅作为视图组件渲染(通过 setComponent 定义)

每个 Field Control 内部使用 Angular Signal 管理状态(与普通组件并行存在):

class FieldControl<TValue> {
  // 值相关
  value$$: Signal<TValue>;
  originValue$$: Signal<TValue | undefined>;

  // 状态
  errors: any;
  dirty: Signal<boolean>;
  touched: Signal<boolean>;
  pristine: Signal<boolean>;

  // 配置
  formConfig: WritableSignal<FieldFormConfig>;
  renderConfig: WritableSignal<RenderConfig>;

  // 方法
  viewValueChange(value: any): void; // 视图层值变化
  updateValue(value: any): void; // 模型层值变化
}

Field Control 树构建完成后,Piying-View 根据 type 映射动态渲染组件:

  1. PiyingView 接收 schemamodeloptions
  2. 调用 convertToField() 构建 Field Control 树
  3. InsertFieldDirective 遍历 Field Control,为每个控件创建组件实例
  4. 使用 NgComponentOutlet 动态挂载用户注册的组件
// PiyingView 内部流程
@Component({
  selector: 'piying-view',
  template: `
    @for (field of fields; track field.id) {
      <ng-container insertField [insertFieldSlots]="field.slots" [insertFieldAttributes]="field.attributes"></ng-container>
    }
  `,
})
export class PiyingView {
  @Input() schema!: BaseSchema;
  @Input() model!: Signal<any>;
  @Input() options!: NgConvertOptions;
  @Output() modelChange = new EventEmitter();
}

Piying-View 将可复用单元划分为四种职责单一的组件类型:

不实现 CVA 契约、不关联 Form Control 的组件,只负责展示与交互,不参与表单值管理。常与非表单控件(NFCSchema / nonFieldControl)配合使用,例如头像、说明文字等纯展示组件,或“发送验证码”这类操作按钮。

实现 CVA 契约的原子化 UI 组件,负责单一值的输入/输出与验证。例如 <input><select> 等表单原生的包装器。

为非包装器组件附加通用能力的装饰性组件,如标签渲染、验证消息展示、前后缀图标等。多个包装器可自由组合,支持以下操作:

  • actions.wrappers.set — 全量替换包装器链
  • actions.wrappers.patch — 增量追加包装器
  • actions.wrappers.remove — 移除指定包装器

处理包含子级字段的 Schema 类型(如 v.object()v.array()v.tuple() 等)。分组组件可自定义布局与样式,例如卡片容器、标签页、手风琴折叠面板等。Piying-View 为所有框架提供了基础的 PiyingViewGroupBase 基类,开发者可按需扩展。


每个字段可以包裹多个 Wrapper,渲染时从外到内依次包裹:

[Wrapper3] → [Wrapper2] → [Wrapper1] → [Field Component]

Wrapper 组件需要注入 InsertFieldDirective 来渲染内部字段。

model value → transformer.toView → view value
view value → pipe(toModel) → transformer.toModel → v.transformer → model value
// PiyingView 模板中的双向绑定
<piying-view
  [(model)]="model"
  (modelChange)="onModelChange($event)"
></piying-view>