在 React 项目中安装并使用 Piying-View 渲染第一个表单。
🚀 想直接看效果? 直接拉模板仓库:https://github.com/piying-org/piying-view-react-template
git clone https://github.com/piying-org/piying-view-react-template
cd piying-view-react-template
npm install && npm run dev
💡 先分清两种模式:本文演示的是自动模式(<PiyingView> + Schema 全自动渲染)。另一种手动模式(convertToField + <PiyingField> 手动绑定)见 两种使用模式。
pnpm add valibot @piying/view-core @piying/view-react
React 没有 NG_VALUE_ACCESSOR 这样的官方注入机制,Piying-View 用一个 Symbol 属性 来完成同样的事:
- 组件接收一个以
CVA(Symbol.for('ControlValueAccessor'))为 key 的 ref 属性
- 组件内部用
useControlValueAccessor() 创建 CVA,再通过 useImperativeHandle 写回这个 ref
// src/piying/input-text.tsx
import { useImperativeHandle } from 'react';
import type { ControlValueAccessor } from '@piying/view-core';
import { CVA, useControlValueAccessor } from '@piying/view-react';
interface PiInputProps {
[CVA]: React.RefObject<ControlValueAccessor>;
}
export function InputText(props: PiInputProps) {
const { cva, cvaa } = useControlValueAccessor();
// 关键:把 cva 写回到 CVA 这个 symbol ref 上
useImperativeHandle(props[CVA], () => cva, [cva]);
return (
<input
className="input"
type="text"
value={cvaa.value ?? ''}
disabled={cvaa.disabled}
onChange={(e) => cvaa.valueChange(e.target.value)}
onBlur={cvaa.touchedChange}
/>
);
}
cvaa 提供:
| 成员 |
类型 |
说明 |
value |
any |
当前值 |
disabled |
boolean |
禁用状态 |
valueChange(v) |
(v) => void |
更新值并触发变更 |
touchedChange() |
() => void |
标记为已触碰 |
React 包提供了一批 use-*Model Hook,把上面那堆 value / onChange 打包成一个可直接展开的对象:
// src/piying/input-text.tsx
import { useImperativeHandle } from 'react';
import type { ControlValueAccessor } from '@piying/view-core';
import { CVA, useControlValueAccessor, useInputTextModel } from '@piying/view-react';
interface PiInputProps {
[CVA]: React.RefObject<ControlValueAccessor>;
}
export function InputText(props: PiInputProps) {
const { cva, cvaa } = useControlValueAccessor();
useImperativeHandle(props[CVA], () => cva, [cva]);
// 第二个参数是 compositionMode:中文输入合成期间是否暂停写值
const textModel = useInputTextModel(cvaa, false);
return <input className="input" type="text" {...textModel} />;
}
照这个模式再写两个控件:
// src/piying/input-number.tsx
import { useImperativeHandle } from 'react';
import type { ControlValueAccessor } from '@piying/view-core';
import { CVA, useControlValueAccessor, useInputNumberModel } from '@piying/view-react';
interface PiInputProps {
[CVA]: React.RefObject<ControlValueAccessor>;
}
export function InputNumber(props: PiInputProps) {
const { cva, cvaa } = useControlValueAccessor();
useImperativeHandle(props[CVA], () => cva, [cva]);
const model = useInputNumberModel(cvaa);
return <input className="input" type="number" {...model} />;
}
// src/piying/input-checkbox.tsx
import { useImperativeHandle } from 'react';
import type { ControlValueAccessor } from '@piying/view-core';
import { CVA, useControlValueAccessor, useInputCheckboxModel } from '@piying/view-react';
interface PiInputProps {
[CVA]: React.RefObject<ControlValueAccessor>;
}
export function InputCheckbox(props: PiInputProps) {
const { cva, cvaa } = useControlValueAccessor();
useImperativeHandle(props[CVA], () => cva, [cva]);
const model = useInputCheckboxModel(cvaa);
return <input className="checkbox" type="checkbox" {...model} />;
}
全部 use-*Model 的签名与差异见 React 字段模型绑定。
包装器(Wrapper)负责在控件外面套一层标签。它通过 React Context 注入当前字段,读取 props['title']。
// src/piying/wrapper/label-wrapper.tsx
import { useContext } from 'react';
import { PI_VIEW_FIELD_TOKEN, useSignalToRef } from '@piying/view-react';
export function LabelWrapper({ children }: { children: React.ReactNode }) {
const field = useContext(PI_VIEW_FIELD_TOKEN)!;
const fieldProps = useSignalToRef(field, (f) => f.props());
return (
<div className="flex items-center gap-2">
{fieldProps['title'] && <span className="label">{fieldProps['title']}</span>}
{children}
</div>
);
}
Wrappers 的完整写法见 Wrappers 包装器。
fieldConfig 是一张「类型名 → 组件」的注册表,Piying-View 按 Schema 推导出的类型名来这里查找渲染组件。
// src/piying/define.ts
import { lazy } from 'react';
import { actions } from '@piying/view-core';
import { PiyingGroup, type PiViewConfig } from '@piying/view-react';
import { InputText } from './input-text';
import { InputNumber } from './input-number';
import { InputCheckbox } from './input-checkbox';
import { LabelWrapper } from './wrapper/label-wrapper';
export const fieldConfig = {
types: {
string: { type: InputText, actions: [actions.wrappers.set(['label'])] },
number: { type: InputNumber, actions: [actions.wrappers.set(['label'])] },
boolean: { type: InputCheckbox, actions: [actions.wrappers.set(['label'])] },
// 对象 / 数组等容器类型,用内置的组容器
object: { type: PiyingGroup },
array: { type: PiyingGroup },
},
wrappers: {
label: { type: LabelWrapper },
},
} as PiViewConfig;
需要懒加载时写成 type: lazy(() => import('./xxx').then((m) => ({ default: m.Xxx })))。
// src/PiyingDemo.tsx
import { useState } from 'react';
import * as v from 'valibot';
import { PiyingView } from '@piying/view-react';
import { fieldConfig } from './piying/define';
const schema = v.object({
name: v.pipe(v.string(), v.minLength(2, '名称至少 2 个字符'), v.title('姓名')),
age: v.pipe(v.number(), v.minValue(18, '必须年满 18 岁'), v.title('年龄')),
email: v.pipe(v.optional(v.string()), v.title('邮箱')),
});
const options = {
fieldGlobalConfig: fieldConfig,
};
export function PiyingDemo() {
const [model, setModel] = useState<Record<string, any>>({});
return (
<>
<PiyingView
schema={schema}
options={options}
model={model}
modelChange={(value) => setModel(value)}
/>
<pre>{JSON.stringify(model, null, 2)}</pre>
</>
);
}
<PiyingView> 接收四个属性:
| 属性 |
说明 |
schema |
Valibot Schema,定义字段和验证规则 |
model |
传入的模型值 |
modelChange |
模型变更回调(仅在无验证错误时触发) |
options |
Options 配置(fieldGlobalConfig 等) |
React 没有 bind: 语法,model 是单向传入、modelChange 回写,需要自己用 useState 串起来。
打开浏览器,你会看到 name / age / email 三个字段的表单。输入数据时 model 会同步更新;校验不通过时不会向 model 写出错误值。