本文介绍如何使用 transformer 和 Valibot 的 transform Action 实现视图层与模型层的值转换。
transform-demo.definition.ts
import * as v from 'valibot' ;
import { actions , formConfig } from '@piying/view-angular-core' ;
export const schema = v. object ({
name : v. pipe (
v. string (),
actions. attributes . set ({ placeholder : '输入前后带空格的值' }),
formConfig ({
transformer : {
toModel : ( value ) => value?. trim () ?? '' ,
},
}),
),
});
export const model = { name : '' };
formConfig.transformer 提供两个转换钩子:
方法
方向
说明
toView
模型 → 视图
存入模型的原始值转换为视图显示值
toModel
视图 → 模型
用户输入的值转换为存入模型的值
transform-to-model.definition.ts
import * as v from 'valibot' ;
import { formConfig } from '@piying/view-angular-core' ;
export const schema = v. object ({
price : v. pipe (
v. string (),
formConfig ({
transformer : {
toModel : ( value : any ) => parseFloat (value),
},
}),
),
});
export const model = { price : '19.99' };
transform-to-view.definition.ts
import * as v from 'valibot' ;
import { formConfig } from '@piying/view-angular-core' ;
export const schema = v. object ({
date : v. pipe (
v. string (),
formConfig ({
transformer : {
toView : ( value : any ) => new Date (value). toLocaleDateString (),
},
}),
),
});
export const model = { date : '2024-01-01T00:00:00.000Z' };
formConfig.pipe.toModel 使用 RxJS Observable 进行值转换,支持去抖、过滤等操作:
transform-pipe.definition.ts
import * as v from 'valibot' ;
import { formConfig } from '@piying/view-angular-core' ;
import { debounceTime , filter , map , pipe } from 'rxjs' ;
export const schema = v. object ({
search : v. pipe (
v. string (),
formConfig ({
pipe : {
toModel : pipe (
debounceTime ( 300 ),
filter (( v ) => v. trim (). length > 0 ),
map (( v ) => v. toLowerCase ()),
),
},
}),
),
});
export const model = { search : 'AbC' };
以下示例节选自单元测试(createBuilder / delay 为测试内部工具,非公开 API;实际项目中用 convertToField 构建字段即可)。
import { map , pipe } from 'rxjs' ;
import { formConfig } from '@piying/view-angular-core' ;
const obj = v. pipe (
v. string (),
formConfig ({
pipe : {
toModel : pipe ( map (( item ) => ` ${ item } 1` )),
},
}),
);
const result = createBuilder (obj);
result. form . control ! . viewValueChange ( '2' );
expect (result. form . control ! . value ). toEqual ( '21' ); // ✅ toModel 转换生效
import { debounceTime , pipe } from 'rxjs' ;
const obj = v. pipe (
v. string (),
formConfig ({
pipe : {
toModel : pipe ( debounceTime ( 1 )),
},
}),
);
const result = createBuilder (obj);
result. form . control ! . viewValueChange ( '1' );
// 去抖期间值未更新
expect (result. form . control ! . value ). not . toEqual ( '1' );
// 等待去抖完成后值生效
await delay ( 2 );
expect (result. form . control ! . value ). toEqual ( '1' );
import { filter , pipe } from 'rxjs' ;
const obj = v. pipe (
v. string (),
formConfig ({
pipe : {
toModel : pipe ( filter (( item ) => item === '1' )),
},
}),
);
const result = createBuilder (obj);
result. form . control ! . viewValueChange ( '1' ); // 通过过滤
result. form . control ! . viewValueChange ( '2' ); // 被过滤掉
expect (result. form . control ! . value ). toEqual ( '1' ); // ✅ 只有 '1' 生效
Valibot 内置的 v.transform() 在 Schema 层面进行值转换,Piying-View 会保留此行为:
transform-basic.definition.ts
import * as v from 'valibot' ;
export const schema = v. object ({
age : v. pipe (
v. string (),
v. transform (( value ) => parseInt (value, 10 )),
),
});
export const model = { age : '25' };
将嵌套对象展平为单个字段:
transform-flatten.definition.ts
import * as v from 'valibot' ;
export const schema = v. object ({
fullName : v. pipe (
v. object ({ first : v. string (), last : v. string () }),
v. transform (( obj ) => ` ${ obj . first } ${ obj . last } ` ),
),
});
export const model = { fullName : '张三' };
transform-array.definition.ts
import * as v from 'valibot' ;
export const schema = v. object ({
tags : v. pipe (
v. array (v. string ()),
v. transform (( arr ) => arr. join ( ',' )),
),
});
export const model = { tags : [ 'a' , 'b' ] };
特性
formConfig.transformer
v.transform()
作用层
Form Control 配置
Valibot Schema
toView
✅ 支持
❌ 不支持(需 v.transform 反向)
toModel
✅ 支持
✅ 支持(等价于 toModel)
Pipe 支持
✅ 完整 RxJS 管道
❌ 不支持
适用场景
UI/模型双向转换
Schema 类型验证 + 转换
transform-complete.definition.ts
import * as v from 'valibot' ;
import { formConfig } from '@piying/view-angular-core' ;
import { debounceTime , filter , map , pipe } from 'rxjs' ;
export const schema = v. object ({
search : v. pipe (
v. string (),
formConfig ({
pipe : {
toModel : pipe (
debounceTime ( 300 ),
filter (( v ) => v. trim (). length > 0 ),
map (( v ) => v?. toLowerCase ()),
),
},
}),
),
price : v. pipe (
v. number (),
formConfig ({
transformer : {
toView : ( value : any ) => (value ? value + 1 : 0 ),
toModel : ( value : any ) => {
return value ? value : value - 1 ;
},
},
}),
),
birthDate : v. pipe (
v. string (),
formConfig ({
transformer : {
toView : ( value : any ) =>
value ? new Date (value). toISOString (). split ( 'T' )[ 0 ] : '' ,
},
}),
),
score : v. pipe (
v. string (),
v. transform (( v ) => parseInt (v, 10 )),
v. minValue ( 0 ),
v. maxValue ( 100 ),
),
});
export const model = { search : '' , price : '' , birthDate : '' , score : '80' };
flowchart LR
A [ 视图变更 ] --> B { 值是否禁用? }
B -->| 是 | C [ 不发射值 ]
B -->| 否 | D [ 触发变更 ]
D --> E [ RxJS pipe.toModel 处理 ]
E --> F [ toModel transformer ]
F --> G [ Valibot 解析验证 ]
G --> H { 验证通过? }
H -->| 失败 | I [ 写入验证异常,标记 dirty ]
H -->| 成功 | J [ 设置表单值 ]
I --> K [ 跳过发射 ]
J --> L [ 触发 valueChanges ]
L --> M [ 异步获取值后发射 ]
flowchart LR
A [ 根级 updateValue ] --> B [ 分发到 FieldControl ]
B --> C { 视图是否变更? }
C -->| 是 | D [ 强制更新 ]
C -->| 否 | E [ 仅与上次不同才更新 ]
D --> F [ toView transformer ]
E --> F
F --> G [ 视图渲染新值 ]
自定义验证 — validators / asyncValidators
数组高级用法 — deletionMode / groupMode
formConfig — transformer / pipe 配置详解