0.1.0 • Published 2 years ago

ramform v0.1.0

Weekly downloads
-
License
ISC
Repository
-
Last release
2 years ago

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;
    }
}

schema page

schema table