0.1.0 • Published 2 years ago
ramform v0.1.0
RAMFORM
一套简单易用可扩展的的动态表单解决方案,及驱动表单的运行时类型系统
功能概览
- 动态表单
- 运行时类型
- 类型注释生成元数据
- 元数据反解成静态类型
开发
npm install xcform
测试
# 安装依赖
npm i
# 执行测试用例
npm test
Document
QuickStart
元数据定义
元数据有两种定义方式,一种是通过解析typescript 类型注释生成,一种是使用工具函数运行时生成
- 基础配置
@title 标注字段名称 @format 标注渲染器类型,format 决定了使用哪种渲染器来渲染该字段
import {Fields, Schema, ValueOf} from 'xcform';
interfae BaseOpt {
title: string;
format: string
}
- string
Fields.string({
enum?: Enum<string>[];
minLength?: number;
maxLength?: number;
pattern?: string;
default?: string;
})
- number
Fields.number({
enum?: Enum<number>[];
minimum?: number;
maximum?: number;
default?: number;
})
- object
Fields.object({
[x: string]: Fields.string()
}, {...options})
- array
Fields.array(Fields.string(), {
maxItems?: number;
minItems?: number;
uniqueItems?: boolean;
})
- enum
Fields.string({
enum: [
Field.enum('a', 'A'),
Field.enum('b', 'B')
]
})
- oneof
Fields.oneOf([
Fields.string(), Fields.number()
]);
typescript类型反解
const SC = Fields.object({
x: Fields.number(),
y: Fields.string()
})
type TypeOfSc = ValueOf<typeof SC>;
const sc: TypeOfSc = getSchemaDftValue(SC);
const x: number = sc.x
const y: string = sc.y
schema 表单
import {FormView} from 'xcform';
<FormView schema={...schema} value={dftValue} onChage={onChange}/>
Schema 表单联动
Watch
@watch 会订阅字段值进行响应式调用, keys 是订阅的字段别名,value 是需要被订阅的字段 订阅字段查找上下文支持顶级object 作用域和相对作用域 顶级object 作用域查找语法 : 以属性名开头,使用'.' |'/' 进行属性分隔 相对作用域查找语法: 以'.' 或'..' 开头,使用'/' 进行属性分隔,查找上下文从 watch 观察者对象字段所在路径开始
{
watch: {
// 顶级作用域
absc: 'a/v/c',
absd: 'a.b.a'
// 相对作用域
relc: './a',
reld: '../../a'
}
}
// @watch 语法,使用JSON键值对标注
interface {
first_name: string;
last_name: string;
schoolInfo: {
name: string;
/**
* 相对作用域查找
* @watch {"fname": "../first_name", "name": "./name"}
**/
address: string;
};
/**
* 顶级作用域查找
* @watch {"fname": "first_name", lname": "last_name", schoolAddress": "schoolInfoaddress"}
**/
fullname: string;
}
Fields.object({
first_name: Fields.string(),
last_name: Fields.string(),
schoolInfo: Fields.object({
name: Fields.string(),
address: Fields.string()
}),
fullname: Fields.string({
watch: {
fname: "first_name",
lname: "last_name",
schoolAddress: "schoolInfo.address"
}
})
})
Template
单纯的watch 不会产生任何作用,通过@template标记可以定义字段之间的关联渲染
interface {
first_name: string;
last_name: string;
schoolInfo: {
name: string;
address: string;
};
/**
* @template {{fname}}-{{lname}}
* @watch {"fname": "first_name", "lname": "last_name"}
**/
fullname: string;
}
Fields.object({
first_name: Fields.string(),
last_name: Fields.string(),
fullname: Fields.string({
template: '{{fname}}-{{lname}}',
watch: {
fname: "first_name",
lname: "last_name"
}
})
})
visibleOn, disableOn
@visibleOn, @disableOn 标注可以定义字段和watch字段之间的显隐关系
Fields.object({
first_name: Fields.string(),
last_name: Fields.string(),
age: Fields.number(),
fullname: Fields.string({
template: '{{fname}}-{{lname}}',
visibleOn: '{{age >= 18}}',
disableOn: '{{age >= 18}}',
watch: {
fname: "first_name",
lname: "last_name",
age: "age"
}
})
})
enumsource
enumsource 标注可以从订阅字段中获取可枚举选项
// schema 定义
interface Country {
name: string;
code: number;
}
interface Student {
countrys: Country[];
/**
* @watch {"allcountrys": "countrys"}
* @enumSource {"source": "allcountrys", "title": "item.title", "value": "item.code"}
* */
curCountry: number;
}
Fields.object({
countrys: Fields.array(Fields.object({
name: Fields.string(),
code: Fields.number()
})),
curGrade: Fields.number({
enumSource: {
source: "allcountrys",
value: "item.code",
title: "item.name"
},
watch: {
allcountrys: "countrys"
}
})
})
inject
inject 标注可以从订阅字段填充字段值, inject 标注接收一个jmespath 查询语句
interface Inject {
questions: string[];
/**
* @watch {"ques":"questions"} // 为mapedQuestions 添加观察者
* @inject ques[].{qid: @} //使用jmespath语法将ques注入进mapedQuestions
*/
mappedQuesiton: {
qid: string;
name: string;
}[];
}
Fields.object({
questions: Fields.array(Fields.string()),
mappedQuesiton: Fields.array(Fields.object({
qid: Fields.string(),
name: Fields.string()
}), {
watch: {
ques: "questions"
},
inject: "ques[].{qid: @} "
})
})
condition
condition 根据订阅字段值动态修改 schema 属性
interface ConditionExample {
answertype: string;
/**
* @watch {"answertype":"answertype"} // 为 items 添加观察者
* @condition {"if": {"answertype": "judge"}, "then": {"items": {"maxItems": 2}}, "else": {"items": {"maxItems": 4}}} //
* 如果 answertype 的值等于 "judge",items 字段的 schema 的 maxItems 属性值为 2,否则为 4
*/
items: string[]
Fields.object({
answertype: Fields.string(),
items: Fields.array(Fields.string(), {
watch: {
answertype: "answertype"
},
condition: {
if: {
answertype: "judge"
},
then: {
items: {
maxItems: 2
}
},
else: {
items: {
maxItems: 4
}
}
}
})
})
options
@options 用于给属性标记扩展选项
interface {
/**
* @options {"collapsed": "true"}
* */
baseinfo: {
name: string;
age: number;
}
}
Fileds.object({
baseinfo: Fields.object({
name: Fields.string(),
name: Fields.number(),
}, {
options: {
collapsed: true
}
})
})
plugins
通过插件修改schema, 当前支持
/**
* @params ctx 观察值的集合
* @params schema 渲染的schema
* @params options 插件名称在schema里设置的值
*/
type EffectFn<T extends any = any> = (ctx: any, schema: Schema, options: string | number | boolean) => T;
effectValue?: EffectFn<string | number | boolean>;
effectEnum?: EffectFn<{value: string; title: string}[]>;
effectSchema?: EffectFn<Schema>;
const Schema = Fields.object({
first_name: Fields.string(),
last_name: Fields.string(),
hobby: Fields.string(),
hobbySelect: Fields.string({
valueToEnum: true,
watch: {
hobby: 'hobby'
}
}),
fullname: Fields.string({
valueToProps: true,
watch: {
fname: 'first_name',
lname: 'last_name'
}
}),
schema: Fields.string(),
changeSchema: Fields.string({
valueToSchema: true,
watch: {
schema: 'schema'
}
})
});
// valueToProps 和 valueToEnum 为插件名称,需在schema 中启用
<FormView
schema={Schema}
plugins={{
effects: {
valueToProps: {
effectValue: (ctx, schema, opts) => {
return JSON.stringify(ctx);
}
},
valueToEnum: {
effectEnum: (ctx, schema, opts) => {
if (!ctx.hobby) return [];
return ctx.hobby.split(',').map((v: string) => ({value: v, title: v}));
}
},
valueToSchema: {
effectSchema: (ctx, schema, opts) => {
if (!ctx.schema) return {...schema, visible: false};
const data = ctx.schema.split(',').map((v: string) => ({value: v, title: v}));
return {...schema, visible: !!data, enum: data};
}
}
}
}}
/>
内置format
TODO: FormLayout
表单元素布局使用grid 布局引擎 通过x-layout-container 指定网格容器 通过x-layout-pos 指定项目位置
interface {
/**
* @x-layout-container {"columns": "2fr 1fr 1fr", "rows": "1fr 1fr 1fr"}
*/
baseinfo: {
/**
* @x-layout-pos {"column": "1/2" , "rows": "1/2"}
* */
name: string;
age: number;
sex: string;
}
}