npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

ramform

v0.1.0

Published

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

Downloads

6

Readme

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