跳转到内容

React 快速开始

在 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 属性 来完成同样的事:

  • 组件接收一个以 CVASymbol.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 包装器

第 4 步:注册类型映射(fieldConfig)

Section titled “第 4 步:注册类型映射(fieldConfig)”

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 串起来。

npm run dev

打开浏览器,你会看到 name / age / email 三个字段的表单。输入数据时 model 会同步更新;校验不通过时不会向 model 写出错误值。