# RAMFORM
 一套简单易用可扩展的的动态表单解决方案，及驱动表单的运行时类型系统

## 功能概览
+ 动态表单
+ 运行时类型
+ 类型注释生成元数据
+ 元数据反解成静态类型
## 开发
```javascript
npm install xcform

```
## 测试
```javascript
# 安装依赖
npm i
# 执行测试用例
npm test
```
## Document
### QuickStart
### 元数据定义
元数据有两种定义方式，一种是通过解析typescript 类型注释生成，一种是使用工具函数运行时生成
+ 基础配置

@title 标注字段名称<br>
@format 标注渲染器类型，format 决定了使用哪种渲染器来渲染该字段<br>

```javascript        
import {Fields, Schema, ValueOf} from 'xcform';
interfae BaseOpt {
    title: string;
    format: string
}
```
+ string

```typescript  
Fields.string({
    enum?: Enum<string>[];
    minLength?: number;
    maxLength?: number;
    pattern?: string;
    default?: string;
})
```

+ number
```typescript  
Fields.number({
    enum?: Enum<number>[];
    minimum?: number;
    maximum?: number;
    default?: number;
})
```

+ object
```typescript  
Fields.object({
    [x: string]: Fields.string()
}, {...options})
```
+ array
```typescript  
Fields.array(Fields.string(), {
    maxItems?: number;
    minItems?: number;
    uniqueItems?: boolean;
})
```

+ enum
```javascript  
Fields.string({
    enum: [
        Field.enum('a', 'A'),
        Field.enum('b', 'B')
    ]
})
```
+ oneof
```typescript  
Fields.oneOf([
    Fields.string(), Fields.number()
]);
```   

### typescript类型反解
```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 表单
```javascript
import {FormView} from 'xcform';
<FormView schema={...schema} value={dftValue} onChage={onChange}/>
```

### Schema 表单联动

#### Watch

@watch 会订阅字段值进行响应式调用，
keys 是订阅的字段别名，value 是需要被订阅的字段
订阅字段查找上下文支持顶级object 作用域和相对作用域
顶级object 作用域查找语法 ： 以属性名开头，使用'.' |'/' 进行属性分隔
相对作用域查找语法： 以'.' 或'..' 开头，使用'/' 进行属性分隔，查找上下文从 watch 观察者对象字段所在路径开始

```typescript
{
    watch: {
        // 顶级作用域
        absc: 'a/v/c',
        absd: 'a.b.a'
        // 相对作用域
        relc: './a',
        reld: '../../a'
    }
}
```
```typescript
// @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;
}

```
```javascript
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标记可以定义字段之间的关联渲染
```typescript
interface {
    first_name: string;
    last_name: string;
    schoolInfo: {
        name: string;
        address: string;
    };
    /**
     * @template {{fname}}-{{lname}}
     * @watch {"fname": "first_name", "lname": "last_name"}
     **/
    fullname: string;
}
```
```javascript
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字段之间的显隐关系
```javascript
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 标注可以从订阅字段中获取可枚举选项

```typescript
// 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;
}

```

```javascript
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 查询语句
```typescript
interface Inject {
    questions: string[];
    /**
     * @watch {"ques":"questions"} // 为mapedQuestions 添加观察者
     * @inject ques[].{qid: @}  //使用jmespath语法将ques注入进mapedQuestions 
     */
     mappedQuesiton: {
        qid: string;
        name: string;
    }[];
}
```
```javascript
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 属性
```typescript
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[]
```
```javascript
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 用于给属性标记扩展选项

```typescript
interface {
    /**
     * @options {"collapsed": "true"}
     * */
    baseinfo: {
        name: string;
        age: number;
    }
}
```
```javascript
Fileds.object({
    baseinfo: Fields.object({
        name: Fields.string(),
        name: Fields.number(),
    }, {
        options: {
            collapsed: true
        }
    })
})
```
#### plugins
通过插件修改schema, 当前支持 
```typescriot
/**
* @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>;
```

````typescript
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 指定项目位置

```typescript
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;
    }
}
```
```javascript
````
### schema page
### schema table