表单是低代码平台中最常用的组件之一,用于数据收集、编辑和提交。本章将详细介绍如何设计和实现一个强大的表单引擎,支持动态表单生成、复杂验证规则、条件显示逻辑等高级特性。
7.1 表单引擎架构概览
7.1.1 整体架构设计
// 表单引擎接口
interface FormEngine {
// 表单管理
createForm(config: FormConfig): Promise<Form>;
getForm(id: string): Form | null;
updateForm(id: string, config: Partial<FormConfig>): Promise<void>;
deleteForm(id: string): Promise<void>;
// 表单渲染
renderForm(formId: string, container: HTMLElement): Promise<FormRenderer>;
// 表单验证
validateForm(formId: string): Promise<ValidationResult>;
// 表单数据
getFormData(formId: string): any;
setFormData(formId: string, data: any): Promise<void>;
// 事件处理
on(event: string, handler: Function): void;
off(event: string, handler: Function): void;
emit(event: string, data: any): void;
}
// 表单引擎实现
class LowCodeFormEngine implements FormEngine {
private forms: Map<string, Form> = new Map();
private renderers: Map<string, FormRenderer> = new Map();
private fieldRegistry: FieldRegistry;
private validationEngine: FormValidationEngine;
private dataEngine: DataEngine;
private eventBus: EventBus;
constructor(config: FormEngineConfig) {
this.fieldRegistry = new FieldRegistry(config.fields || []);
this.validationEngine = new FormValidationEngine(config.validation || {});
this.dataEngine = config.dataEngine;
this.eventBus = new EventBus();
this.initializeBuiltinFields();
}
async createForm(config: FormConfig): Promise<Form> {
// 验证配置
this.validateFormConfig(config);
// 创建表单实例
const form = new Form({
...config,
engine: this,
fieldRegistry: this.fieldRegistry,
validationEngine: this.validationEngine,
dataEngine: this.dataEngine
});
// 初始化表单
await form.initialize();
// 注册表单
this.forms.set(form.id, form);
// 触发事件
this.emit('form:created', { form });
return form;
}
getForm(id: string): Form | null {
return this.forms.get(id) || null;
}
async updateForm(id: string, config: Partial<FormConfig>): Promise<void> {
const form = this.getForm(id);
if (!form) {
throw new Error(`表单未找到: ${id}`);
}
await form.update(config);
this.emit('form:updated', { form, config });
}
async deleteForm(id: string): Promise<void> {
const form = this.getForm(id);
if (!form) {
return;
}
// 清理渲染器
const renderer = this.renderers.get(id);
if (renderer) {
await renderer.destroy();
this.renderers.delete(id);
}
// 销毁表单
await form.destroy();
this.forms.delete(id);
this.emit('form:deleted', { formId: id });
}
async renderForm(formId: string, container: HTMLElement): Promise<FormRenderer> {
const form = this.getForm(formId);
if (!form) {
throw new Error(`表单未找到: ${formId}`);
}
// 创建渲染器
const renderer = new FormRenderer({
form,
container,
fieldRegistry: this.fieldRegistry,
eventBus: this.eventBus
});
// 渲染表单
await renderer.render();
// 注册渲染器
this.renderers.set(formId, renderer);
return renderer;
}
async validateForm(formId: string): Promise<ValidationResult> {
const form = this.getForm(formId);
if (!form) {
throw new Error(`表单未找到: ${formId}`);
}
return await form.validate();
}
getFormData(formId: string): any {
const form = this.getForm(formId);
if (!form) {
throw new Error(`表单未找到: ${formId}`);
}
return form.getData();
}
async setFormData(formId: string, data: any): Promise<void> {
const form = this.getForm(formId);
if (!form) {
throw new Error(`表单未找到: ${formId}`);
}
await form.setData(data);
}
// 事件处理
on(event: string, handler: Function): void {
this.eventBus.on(event, handler);
}
off(event: string, handler: Function): void {
this.eventBus.off(event, handler);
}
emit(event: string, data: any): void {
this.eventBus.emit(event, data);
}
// 验证表单配置
private validateFormConfig(config: FormConfig): void {
if (!config.id) {
throw new Error('表单ID不能为空');
}
if (!config.fields || config.fields.length === 0) {
throw new Error('表单字段不能为空');
}
// 验证字段配置
config.fields.forEach(field => {
if (!field.name) {
throw new Error('字段名称不能为空');
}
if (!field.type) {
throw new Error(`字段 ${field.name} 的类型不能为空`);
}
if (!this.fieldRegistry.hasField(field.type)) {
throw new Error(`未知的字段类型: ${field.type}`);
}
});
}
// 初始化内置字段
private initializeBuiltinFields(): void {
// 注册内置字段类型
this.fieldRegistry.register(new TextFieldDefinition());
this.fieldRegistry.register(new NumberFieldDefinition());
this.fieldRegistry.register(new EmailFieldDefinition());
this.fieldRegistry.register(new PasswordFieldDefinition());
this.fieldRegistry.register(new TextareaFieldDefinition());
this.fieldRegistry.register(new SelectFieldDefinition());
this.fieldRegistry.register(new CheckboxFieldDefinition());
this.fieldRegistry.register(new RadioFieldDefinition());
this.fieldRegistry.register(new DateFieldDefinition());
this.fieldRegistry.register(new FileFieldDefinition());
this.fieldRegistry.register(new SwitchFieldDefinition());
this.fieldRegistry.register(new SliderFieldDefinition());
this.fieldRegistry.register(new RateFieldDefinition());
this.fieldRegistry.register(new ColorFieldDefinition());
this.fieldRegistry.register(new CascaderFieldDefinition());
this.fieldRegistry.register(new TreeSelectFieldDefinition());
this.fieldRegistry.register(new TransferFieldDefinition());
this.fieldRegistry.register(new UploadFieldDefinition());
}
}
7.6 完整使用示例
7.6.1 表单引擎集成示例
// 完整的表单引擎使用示例
class LowCodeFormEngineDemo {
private formEngine: LowCodeFormEngine;
private eventBus: EventBus;
private container: HTMLElement;
constructor(container: HTMLElement) {
this.container = container;
this.eventBus = new EventBus();
this.formEngine = new LowCodeFormEngine({
eventBus: this.eventBus,
validationEnabled: true,
autoSave: true,
autoSaveInterval: 5000
});
this.initializeDemo();
}
private async initializeDemo(): Promise<void> {
// 创建用户注册表单
await this.createUserRegistrationForm();
// 创建产品信息表单
await this.createProductForm();
// 绑定事件监听
this.bindEvents();
}
private async createUserRegistrationForm(): Promise<void> {
// 创建表单配置
const formConfig: FormConfig = {
id: 'user-registration',
title: '用户注册',
description: '请填写以下信息完成注册',
layout: {
type: LayoutType.GRID,
columns: 2,
gap: '16px',
responsive: true,
breakpoints: {
xs: 1,
sm: 1,
md: 2,
lg: 2,
xl: 2
}
},
submitUrl: '/api/users/register',
method: 'POST'
};
// 创建表单
const form = await this.formEngine.createForm(formConfig);
// 添加用户名字段
const usernameField = await this.formEngine.createField({
name: 'username',
type: FieldType.TEXT,
label: '用户名',
placeholder: '请输入用户名',
required: true,
gridColumn: 'span 2'
});
// 添加验证规则
const validationEngine = new FormValidationEngine(form);
validationEngine.addRule('username', {
type: ValidationRuleType.REQUIRED,
message: '用户名不能为空'
});
validationEngine.addRule('username', {
type: ValidationRuleType.MIN_LENGTH,
value: 3,
message: '用户名至少需要3个字符'
});
validationEngine.addRule('username', {
type: ValidationRuleType.PATTERN,
value: /^[a-zA-Z0-9_]+$/,
message: '用户名只能包含字母、数字和下划线'
});
form.addField(usernameField);
// 添加邮箱字段
const emailField = await this.formEngine.createField({
name: 'email',
type: FieldType.TEXT,
label: '邮箱地址',
placeholder: '请输入邮箱地址',
required: true
});
validationEngine.addRule('email', {
type: ValidationRuleType.REQUIRED,
message: '邮箱地址不能为空'
});
validationEngine.addRule('email', {
type: ValidationRuleType.EMAIL,
message: '请输入有效的邮箱地址'
});
form.addField(emailField);
// 添加手机号字段
const phoneField = await this.formEngine.createField({
name: 'phone',
type: FieldType.TEXT,
label: '手机号码',
placeholder: '请输入手机号码',
required: true
});
validationEngine.addRule('phone', {
type: ValidationRuleType.REQUIRED,
message: '手机号码不能为空'
});
validationEngine.addRule('phone', {
type: ValidationRuleType.CUSTOM,
message: '请输入有效的手机号码',
validator: (value: string) => {
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(value);
}
});
form.addField(phoneField);
// 添加密码字段
const passwordField = await this.formEngine.createField({
name: 'password',
type: FieldType.PASSWORD,
label: '密码',
placeholder: '请输入密码',
required: true,
gridColumn: 'span 2'
});
validationEngine.addRule('password', {
type: ValidationRuleType.REQUIRED,
message: '密码不能为空'
});
validationEngine.addRule('password', {
type: ValidationRuleType.MIN_LENGTH,
value: 8,
message: '密码至少需要8个字符'
});
validationEngine.addRule('password', {
type: ValidationRuleType.PATTERN,
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/,
message: '密码必须包含大小写字母和数字'
});
form.addField(passwordField);
// 添加确认密码字段
const confirmPasswordField = await this.formEngine.createField({
name: 'confirmPassword',
type: FieldType.PASSWORD,
label: '确认密码',
placeholder: '请再次输入密码',
required: true,
gridColumn: 'span 2'
});
validationEngine.addRule('confirmPassword', {
type: ValidationRuleType.REQUIRED,
message: '确认密码不能为空'
});
validationEngine.addRule('confirmPassword', {
type: ValidationRuleType.CUSTOM,
message: '两次输入的密码不一致',
validator: (value: string) => {
const password = form.getFieldValue('password');
return value === password;
}
});
form.addField(confirmPasswordField);
// 添加性别字段
const genderField = await this.formEngine.createField({
name: 'gender',
type: FieldType.SELECT,
label: '性别',
options: [
{ value: 'male', label: '男' },
{ value: 'female', label: '女' },
{ value: 'other', label: '其他' }
],
required: true
});
form.addField(genderField);
// 添加生日字段
const birthdayField = await this.formEngine.createField({
name: 'birthday',
type: FieldType.DATE,
label: '生日',
required: false
});
form.addField(birthdayField);
// 添加头像上传字段
const avatarField = await this.formEngine.createField({
name: 'avatar',
type: FieldType.FILE,
label: '头像',
accept: 'image/*',
maxSize: 2 * 1024 * 1024, // 2MB
gridColumn: 'span 2'
});
form.addField(avatarField);
// 渲染表单
const formContainer = document.createElement('div');
formContainer.className = 'form-container';
this.container.appendChild(formContainer);
await this.formEngine.renderForm(form.id, formContainer);
}
private async createProductForm(): Promise<void> {
// 创建产品表单配置
const formConfig: FormConfig = {
id: 'product-form',
title: '产品信息',
description: '请填写产品的详细信息',
layout: {
type: LayoutType.TABS
},
submitUrl: '/api/products',
method: 'POST'
};
// 创建表单
const form = await this.formEngine.createForm(formConfig);
// 基本信息标签页
const basicInfoTab = {
id: 'basic-info',
title: '基本信息',
fields: [
{
name: 'name',
type: FieldType.TEXT,
label: '产品名称',
required: true
},
{
name: 'category',
type: FieldType.SELECT,
label: '产品分类',
options: [
{ value: 'electronics', label: '电子产品' },
{ value: 'clothing', label: '服装' },
{ value: 'books', label: '图书' },
{ value: 'home', label: '家居用品' }
],
required: true
},
{
name: 'price',
type: FieldType.NUMBER,
label: '价格',
required: true
},
{
name: 'description',
type: FieldType.TEXTAREA,
label: '产品描述',
rows: 4
}
]
};
// 详细信息标签页
const detailInfoTab = {
id: 'detail-info',
title: '详细信息',
fields: [
{
name: 'specifications',
type: FieldType.TEXTAREA,
label: '产品规格',
rows: 6
},
{
name: 'images',
type: FieldType.FILE,
label: '产品图片',
accept: 'image/*',
multiple: true,
maxCount: 5
},
{
name: 'tags',
type: FieldType.TEXT,
label: '标签',
placeholder: '用逗号分隔多个标签'
}
]
};
// 添加字段到表单
for (const tab of [basicInfoTab, detailInfoTab]) {
for (const fieldConfig of tab.fields) {
const field = await this.formEngine.createField(fieldConfig);
form.addField(field);
}
}
// 渲染表单
const formContainer = document.createElement('div');
formContainer.className = 'form-container';
formContainer.style.marginTop = '32px';
this.container.appendChild(formContainer);
await this.formEngine.renderForm(form.id, formContainer);
}
private bindEvents(): void {
// 监听表单提交成功
this.eventBus.on('form:submitSuccess', (event) => {
console.log('表单提交成功:', event.result);
this.showMessage('表单提交成功!', 'success');
});
// 监听表单提交失败
this.eventBus.on('form:submitError', (event) => {
console.error('表单提交失败:', event.error || event.result);
this.showMessage('表单提交失败,请检查输入信息', 'error');
});
// 监听表单验证
this.eventBus.on('form:validated', (event) => {
if (!event.result.valid) {
console.log('表单验证失败:', event.result.errors);
}
});
// 监听字段值变化
this.eventBus.on('field:valueChanged', (event) => {
console.log(`字段 ${event.field.name} 值变化:`, event.value);
});
// 监听表单重置
this.eventBus.on('form:reset', (event) => {
console.log('表单已重置:', event.form.id);
this.showMessage('表单已重置', 'info');
});
}
private showMessage(message: string, type: 'success' | 'error' | 'info'): void {
const messageElement = document.createElement('div');
messageElement.className = `message message-${type}`;
messageElement.textContent = message;
messageElement.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
border-radius: 4px;
color: white;
font-weight: 500;
z-index: 1000;
animation: slideIn 0.3s ease-out;
`;
switch (type) {
case 'success':
messageElement.style.backgroundColor = '#52c41a';
break;
case 'error':
messageElement.style.backgroundColor = '#ff4d4f';
break;
case 'info':
messageElement.style.backgroundColor = '#1890ff';
break;
}
document.body.appendChild(messageElement);
// 3秒后自动移除
setTimeout(() => {
if (messageElement.parentNode) {
messageElement.parentNode.removeChild(messageElement);
}
}, 3000);
}
}
// 使用示例
const demo = new LowCodeFormEngineDemo(document.getElementById('app')!);
7.7 本章小结
在本章中,我们深入学习了低代码平台中表单引擎与动态表单的实现。通过系统性的架构设计和详细的代码实现,我们构建了一个功能完整、扩展性强的表单系统。
7.7.1 核心要点回顾
表单引擎架构: - 采用模块化设计,包含表单管理、字段管理、渲染引擎、验证引擎等核心模块 - 基于事件驱动架构,实现组件间的松耦合通信 - 支持插件化扩展,可以灵活添加新的字段类型和功能
动态表单字段: - 实现了文本、密码、数字、选择、日期、文件等多种字段类型 - 每个字段都支持完整的生命周期管理和事件处理 - 提供统一的字段接口,便于扩展新的字段类型
表单渲染引擎: - 支持多种布局方式:垂直、水平、网格、标签页、手风琴 - 实现响应式布局,适配不同屏幕尺寸 - 提供灵活的样式定制和主题支持
表单验证引擎: - 内置常用验证规则:必填、长度、格式、数值范围等 - 支持自定义验证器和异步验证 - 提供实时验证和批量验证功能
7.7.2 技术特色
- 类型安全:使用 TypeScript 提供完整的类型定义和检查
- 事件驱动:基于事件总线实现组件间通信,提高系统的可维护性
- 插件化架构:支持字段类型、验证器、布局引擎的插件化扩展
- 响应式设计:支持多种布局方式和响应式适配
- 性能优化:实现字段的懒加载和按需渲染
7.7.3 最佳实践
- 字段设计:遵循单一职责原则,每个字段专注于特定的数据类型
- 验证策略:结合前端验证和后端验证,确保数据安全性
- 用户体验:提供实时反馈、错误提示和操作引导
- 性能考虑:对于大型表单,采用虚拟滚动和分页加载
- 可访问性:遵循 WCAG 标准,支持键盘导航和屏幕阅读器
7.7.4 扩展方向
- 高级字段类型:富文本编辑器、代码编辑器、地图选择器等
- 表单模板:预定义的表单模板和快速生成工具
- 数据联动:字段间的依赖关系和联动更新
- 表单分析:用户行为分析和表单优化建议
- 国际化支持:多语言表单和本地化适配
通过本章的学习,我们掌握了表单引擎的核心技术和实现方法。在下一章中,我们将学习工作流引擎与流程管理,了解如何在低代码平台中实现复杂的业务流程自动化。
// 表单配置 interface FormConfig { id: string; name: string; title?: string; description?: string; fields: FieldConfig[]; layout?: FormLayout; validation?: FormValidationConfig; submission?: FormSubmissionConfig; styling?: FormStylingConfig; behavior?: FormBehaviorConfig; dataSource?: DataSourceConfig; }
// 表单引擎配置 interface FormEngineConfig { fields?: FieldDefinition[]; validation?: ValidationEngineConfig; dataEngine: DataEngine; }
### 7.1.2 核心数据结构
```typescript
// 表单类
class Form {
public readonly id: string;
public readonly name: string;
public title?: string;
public description?: string;
private fields: Map<string, FormField> = new Map();
private data: any = {};
private errors: Map<string, string[]> = new Map();
private config: FormConfig;
private engine: FormEngine;
private fieldRegistry: FieldRegistry;
private validationEngine: FormValidationEngine;
private dataEngine: DataEngine;
private eventBus: EventBus;
constructor(options: FormOptions) {
this.id = options.id;
this.name = options.name;
this.title = options.title;
this.description = options.description;
this.config = options.config;
this.engine = options.engine;
this.fieldRegistry = options.fieldRegistry;
this.validationEngine = options.validationEngine;
this.dataEngine = options.dataEngine;
this.eventBus = new EventBus();
}
async initialize(): Promise<void> {
// 创建字段实例
for (const fieldConfig of this.config.fields) {
const fieldDefinition = this.fieldRegistry.getField(fieldConfig.type);
if (!fieldDefinition) {
throw new Error(`未知的字段类型: ${fieldConfig.type}`);
}
const field = new FormField({
config: fieldConfig,
definition: fieldDefinition,
form: this,
validationEngine: this.validationEngine
});
await field.initialize();
this.fields.set(field.name, field);
}
// 初始化数据
this.initializeData();
// 设置字段依赖关系
this.setupFieldDependencies();
// 绑定数据源
if (this.config.dataSource) {
await this.bindDataSource();
}
}
async update(config: Partial<FormConfig>): Promise<void> {
// 更新配置
this.config = { ...this.config, ...config };
// 重新初始化
await this.initialize();
this.emit('form:updated', { form: this });
}
async validate(): Promise<ValidationResult> {
const errors: ValidationError[] = [];
// 验证所有字段
for (const field of this.fields.values()) {
const fieldResult = await field.validate();
if (!fieldResult.valid) {
errors.push(...fieldResult.errors);
}
}
// 表单级验证
if (this.config.validation?.rules) {
const formResult = await this.validationEngine.validateForm(
this.data,
this.config.validation.rules
);
if (!formResult.valid) {
errors.push(...formResult.errors);
}
}
// 更新错误状态
this.updateErrors(errors);
const result = {
valid: errors.length === 0,
errors
};
this.emit('form:validated', { form: this, result });
return result;
}
getData(): any {
const data: any = {};
for (const field of this.fields.values()) {
if (field.isVisible() && !field.isDisabled()) {
this.setNestedValue(data, field.name, field.getValue());
}
}
return data;
}
async setData(data: any): Promise<void> {
this.data = { ...data };
// 更新字段值
for (const field of this.fields.values()) {
const value = this.getNestedValue(data, field.name);
if (value !== undefined) {
await field.setValue(value);
}
}
this.emit('form:dataChanged', { form: this, data });
}
getField(name: string): FormField | null {
return this.fields.get(name) || null;
}
getFields(): FormField[] {
return Array.from(this.fields.values());
}
async submit(): Promise<SubmissionResult> {
// 验证表单
const validation = await this.validate();
if (!validation.valid) {
return {
success: false,
errors: validation.errors
};
}
// 获取提交数据
const data = this.getData();
// 执行提交逻辑
if (this.config.submission) {
try {
const result = await this.executeSubmission(data);
this.emit('form:submitted', { form: this, data, result });
return result;
} catch (error) {
const result = {
success: false,
error: error.message
};
this.emit('form:submitError', { form: this, data, error });
return result;
}
}
// 默认成功
const result = { success: true, data };
this.emit('form:submitted', { form: this, data, result });
return result;
}
async reset(): Promise<void> {
// 重置所有字段
for (const field of this.fields.values()) {
await field.reset();
}
// 清除错误
this.errors.clear();
// 重新初始化数据
this.initializeData();
this.emit('form:reset', { form: this });
}
async destroy(): Promise<void> {
// 销毁所有字段
for (const field of this.fields.values()) {
await field.destroy();
}
this.fields.clear();
this.errors.clear();
this.emit('form:destroyed', { form: this });
}
// 事件处理
on(event: string, handler: Function): void {
this.eventBus.on(event, handler);
}
off(event: string, handler: Function): void {
this.eventBus.off(event, handler);
}
emit(event: string, data: any): void {
this.eventBus.emit(event, data);
}
// 私有方法
private initializeData(): void {
this.data = {};
for (const field of this.fields.values()) {
const defaultValue = field.getDefaultValue();
if (defaultValue !== undefined) {
this.setNestedValue(this.data, field.name, defaultValue);
}
}
}
private setupFieldDependencies(): void {
for (const field of this.fields.values()) {
const dependencies = field.getDependencies();
for (const dependency of dependencies) {
const dependentField = this.getField(dependency);
if (dependentField) {
dependentField.on('value:changed', () => {
field.updateVisibility();
field.updateDisabled();
field.updateOptions();
});
}
}
}
}
private async bindDataSource(): Promise<void> {
if (!this.config.dataSource) {
return;
}
try {
const data = await this.dataEngine.query(
this.config.dataSource.id,
this.config.dataSource.params
);
if (data) {
await this.setData(data);
}
} catch (error) {
console.error('数据源绑定失败:', error);
}
}
private async executeSubmission(data: any): Promise<SubmissionResult> {
const submission = this.config.submission!;
switch (submission.type) {
case 'api':
return await this.submitToApi(data, submission.config as ApiSubmissionConfig);
case 'dataSource':
return await this.submitToDataSource(data, submission.config as DataSourceSubmissionConfig);
case 'custom':
return await this.submitCustom(data, submission.config as CustomSubmissionConfig);
default:
throw new Error(`未知的提交类型: ${submission.type}`);
}
}
private async submitToApi(data: any, config: ApiSubmissionConfig): Promise<SubmissionResult> {
const response = await fetch(config.url, {
method: config.method || 'POST',
headers: {
'Content-Type': 'application/json',
...config.headers
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`API提交失败: ${response.statusText}`);
}
const result = await response.json();
return {
success: true,
data: result
};
}
private async submitToDataSource(data: any, config: DataSourceSubmissionConfig): Promise<SubmissionResult> {
const result = await this.dataEngine.execute(config.dataSourceId, {
operation: config.operation || 'create',
data
});
return {
success: result.success,
data: result.data,
error: result.error
};
}
private async submitCustom(data: any, config: CustomSubmissionConfig): Promise<SubmissionResult> {
return await config.handler(data, this);
}
private updateErrors(errors: ValidationError[]): void {
this.errors.clear();
for (const error of errors) {
const fieldErrors = this.errors.get(error.field) || [];
fieldErrors.push(error.message);
this.errors.set(error.field, fieldErrors);
}
}
private getNestedValue(obj: any, path: string): any {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current === null || current === undefined) {
return undefined;
}
current = current[key];
}
return current;
}
private setNestedValue(obj: any, path: string, value: any): void {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current) || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
}
// 表单选项
interface FormOptions {
id: string;
name: string;
title?: string;
description?: string;
config: FormConfig;
engine: FormEngine;
fieldRegistry: FieldRegistry;
validationEngine: FormValidationEngine;
dataEngine: DataEngine;
}
// 提交结果
interface SubmissionResult {
success: boolean;
data?: any;
error?: string;
errors?: ValidationError[];
}
// 提交配置
interface FormSubmissionConfig {
type: 'api' | 'dataSource' | 'custom';
config: ApiSubmissionConfig | DataSourceSubmissionConfig | CustomSubmissionConfig;
}
interface ApiSubmissionConfig {
url: string;
method?: string;
headers?: Record<string, string>;
}
interface DataSourceSubmissionConfig {
dataSourceId: string;
operation?: string;
}
interface CustomSubmissionConfig {
handler: (data: any, form: Form) => Promise<SubmissionResult>;
}
7.2 字段注册系统
7.2.1 字段注册器
// 字段注册器
class FieldRegistry {
private fields: Map<string, FieldDefinition> = new Map();
private categories: Map<string, FieldCategory> = new Map();
constructor(fields: FieldDefinition[] = []) {
fields.forEach(field => this.register(field));
this.initializeCategories();
}
// 注册字段
register(field: FieldDefinition): void {
this.fields.set(field.type, field);
// 添加到分类
const category = this.categories.get(field.category) || {
id: field.category,
name: field.category,
fields: []
};
if (!category.fields.includes(field.type)) {
category.fields.push(field.type);
}
this.categories.set(field.category, category);
}
// 获取字段定义
getField(type: string): FieldDefinition | null {
return this.fields.get(type) || null;
}
// 检查字段是否存在
hasField(type: string): boolean {
return this.fields.has(type);
}
// 获取所有字段
getAllFields(): FieldDefinition[] {
return Array.from(this.fields.values());
}
// 按分类获取字段
getFieldsByCategory(category: string): FieldDefinition[] {
const categoryInfo = this.categories.get(category);
if (!categoryInfo) {
return [];
}
return categoryInfo.fields
.map(type => this.getField(type))
.filter(field => field !== null) as FieldDefinition[];
}
// 获取所有分类
getCategories(): FieldCategory[] {
return Array.from(this.categories.values());
}
// 搜索字段
searchFields(query: string): FieldDefinition[] {
const lowerQuery = query.toLowerCase();
return this.getAllFields().filter(field =>
field.name.toLowerCase().includes(lowerQuery) ||
field.type.toLowerCase().includes(lowerQuery) ||
(field.description && field.description.toLowerCase().includes(lowerQuery))
);
}
// 初始化分类
private initializeCategories(): void {
const defaultCategories = [
{ id: 'basic', name: '基础字段', fields: [] },
{ id: 'input', name: '输入字段', fields: [] },
{ id: 'selection', name: '选择字段', fields: [] },
{ id: 'display', name: '显示字段', fields: [] },
{ id: 'layout', name: '布局字段', fields: [] },
{ id: 'advanced', name: '高级字段', fields: [] }
];
defaultCategories.forEach(category => {
if (!this.categories.has(category.id)) {
this.categories.set(category.id, category);
}
});
}
}
// 字段分类
interface FieldCategory {
id: string;
name: string;
fields: string[];
}
// 字段定义基类
abstract class FieldDefinition {
abstract readonly type: string;
abstract readonly name: string;
abstract readonly category: string;
abstract readonly icon?: string;
abstract readonly description?: string;
// 创建字段实例
abstract createField(config: FieldConfig): FormField;
// 获取默认配置
abstract getDefaultConfig(): Partial<FieldConfig>;
// 获取配置模式
abstract getConfigSchema(): FieldConfigSchema;
// 验证配置
validateConfig(config: FieldConfig): ValidationResult {
const schema = this.getConfigSchema();
return this.validateAgainstSchema(config, schema);
}
// 根据模式验证
private validateAgainstSchema(config: any, schema: FieldConfigSchema): ValidationResult {
const errors: ValidationError[] = [];
// 验证必填属性
for (const required of schema.required || []) {
if (!(required in config) || config[required] === undefined) {
errors.push({
field: required,
rule: ValidationRuleType.REQUIRED,
message: `${required} 是必填属性`,
value: config[required]
});
}
}
// 验证属性类型
for (const [property, propertySchema] of Object.entries(schema.properties || {})) {
if (property in config) {
const value = config[property];
if (!this.validatePropertyType(value, propertySchema)) {
errors.push({
field: property,
rule: ValidationRuleType.TYPE,
message: `${property} 类型不正确`,
value
});
}
}
}
return {
valid: errors.length === 0,
errors
};
}
private validatePropertyType(value: any, schema: PropertySchema): boolean {
switch (schema.type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number';
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
return typeof value === 'object' && !Array.isArray(value);
default:
return true;
}
}
}
// 字段配置模式
interface FieldConfigSchema {
properties?: Record<string, PropertySchema>;
required?: string[];
}
interface PropertySchema {
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
description?: string;
default?: any;
enum?: any[];
}
7.2.2 内置字段定义
// 文本字段定义
class TextFieldDefinition extends FieldDefinition {
readonly type = 'text';
readonly name = '文本输入';
readonly category = 'input';
readonly icon = 'text';
readonly description = '单行文本输入字段';
createField(config: FieldConfig): FormField {
return new TextField(config, this);
}
getDefaultConfig(): Partial<FieldConfig> {
return {
placeholder: '请输入文本',
maxLength: 255,
clearable: true
};
}
getConfigSchema(): FieldConfigSchema {
return {
properties: {
placeholder: { type: 'string', description: '占位符文本' },
maxLength: { type: 'number', description: '最大长度' },
minLength: { type: 'number', description: '最小长度' },
clearable: { type: 'boolean', description: '是否可清除' },
prefix: { type: 'string', description: '前缀图标' },
suffix: { type: 'string', description: '后缀图标' }
},
required: ['name', 'type']
};
}
}
// 数字字段定义
class NumberFieldDefinition extends FieldDefinition {
readonly type = 'number';
readonly name = '数字输入';
readonly category = 'input';
readonly icon = 'number';
readonly description = '数字输入字段';
createField(config: FieldConfig): FormField {
return new NumberField(config, this);
}
getDefaultConfig(): Partial<FieldConfig> {
return {
placeholder: '请输入数字',
step: 1,
precision: 0
};
}
getConfigSchema(): FieldConfigSchema {
return {
properties: {
placeholder: { type: 'string', description: '占位符文本' },
min: { type: 'number', description: '最小值' },
max: { type: 'number', description: '最大值' },
step: { type: 'number', description: '步长' },
precision: { type: 'number', description: '精度' },
controls: { type: 'boolean', description: '是否显示控制按钮' }
},
required: ['name', 'type']
};
}
}
// 选择字段定义
class SelectFieldDefinition extends FieldDefinition {
readonly type = 'select';
readonly name = '下拉选择';
readonly category = 'selection';
readonly icon = 'select';
readonly description = '下拉选择字段';
createField(config: FieldConfig): FormField {
return new SelectField(config, this);
}
getDefaultConfig(): Partial<FieldConfig> {
return {
placeholder: '请选择',
clearable: true,
filterable: false,
multiple: false,
options: []
};
}
getConfigSchema(): FieldConfigSchema {
return {
properties: {
placeholder: { type: 'string', description: '占位符文本' },
clearable: { type: 'boolean', description: '是否可清除' },
filterable: { type: 'boolean', description: '是否可搜索' },
multiple: { type: 'boolean', description: '是否多选' },
options: { type: 'array', description: '选项列表' },
optionsSource: { type: 'object', description: '选项数据源' }
},
required: ['name', 'type']
};
}
}
// 日期字段定义
class DateFieldDefinition extends FieldDefinition {
readonly type = 'date';
readonly name = '日期选择';
readonly category = 'input';
readonly icon = 'date';
readonly description = '日期选择字段';
createField(config: FieldConfig): FormField {
return new DateField(config, this);
}
getDefaultConfig(): Partial<FieldConfig> {
return {
placeholder: '请选择日期',
format: 'YYYY-MM-DD',
clearable: true
};
}
getConfigSchema(): FieldConfigSchema {
return {
properties: {
placeholder: { type: 'string', description: '占位符文本' },
format: { type: 'string', description: '日期格式' },
clearable: { type: 'boolean', description: '是否可清除' },
disabledDate: { type: 'object', description: '禁用日期函数' },
shortcuts: { type: 'array', description: '快捷选项' }
},
required: ['name', 'type']
};
}
}
// 文件上传字段定义
class FileFieldDefinition extends FieldDefinition {
readonly type = 'file';
readonly name = '文件上传';
readonly category = 'input';
readonly icon = 'upload';
readonly description = '文件上传字段';
createField(config: FieldConfig): FormField {
return new FileField(config, this);
}
getDefaultConfig(): Partial<FieldConfig> {
return {
accept: '*',
multiple: false,
maxSize: 10 * 1024 * 1024, // 10MB
maxCount: 1
};
}
getConfigSchema(): FieldConfigSchema {
return {
properties: {
accept: { type: 'string', description: '接受的文件类型' },
multiple: { type: 'boolean', description: '是否多选' },
maxSize: { type: 'number', description: '最大文件大小' },
maxCount: { type: 'number', description: '最大文件数量' },
uploadUrl: { type: 'string', description: '上传地址' },
headers: { type: 'object', description: '上传请求头' }
},
required: ['name', 'type']
};
}
}
7.3 表单字段实现
7.3.1 字段基类
// 表单字段基类
abstract class FormField {
public readonly name: string;
public readonly type: string;
public readonly label: string;
protected config: FieldConfig;
protected definition: FieldDefinition;
protected form: Form;
protected validationEngine: FormValidationEngine;
protected eventBus: EventBus;
protected element: HTMLElement | null = null;
protected value: any;
protected defaultValue: any;
protected errors: string[] = [];
protected visible: boolean = true;
protected disabled: boolean = false;
protected readonly: boolean = false;
constructor(options: FieldOptions) {
this.config = options.config;
this.definition = options.definition;
this.form = options.form;
this.validationEngine = options.validationEngine;
this.eventBus = new EventBus();
this.name = this.config.name;
this.type = this.config.type;
this.label = this.config.label || this.name;
this.defaultValue = this.config.defaultValue;
this.value = this.defaultValue;
this.visible = this.config.visible !== false;
this.disabled = this.config.disabled === true;
this.readonly = this.config.readonly === true;
}
async initialize(): Promise<void> {
// 初始化字段特定逻辑
await this.initializeField();
// 设置初始值
if (this.defaultValue !== undefined) {
await this.setValue(this.defaultValue);
}
}
// 抽象方法 - 子类必须实现
abstract render(container: HTMLElement): Promise<HTMLElement>;
abstract getValue(): any;
abstract setValue(value: any): Promise<void>;
abstract focus(): void;
abstract blur(): void;
abstract clear(): void;
// 字段初始化 - 子类可重写
protected async initializeField(): Promise<void> {
// 默认实现为空
}
// 验证字段
async validate(): Promise<ValidationResult> {
const errors: ValidationError[] = [];
const value = this.getValue();
// 必填验证
if (this.config.required && this.isEmpty(value)) {
errors.push({
field: this.name,
rule: ValidationRuleType.REQUIRED,
message: this.config.requiredMessage || `${this.label} 是必填字段`,
value
});
}
// 字段特定验证
if (!this.isEmpty(value)) {
const fieldErrors = await this.validateField(value);
errors.push(...fieldErrors);
}
// 自定义验证规则
if (this.config.rules) {
for (const rule of this.config.rules) {
const ruleResult = await this.validationEngine.validateRule(value, rule);
if (!ruleResult.valid) {
errors.push(...ruleResult.errors);
}
}
}
// 更新错误状态
this.errors = errors.map(error => error.message);
this.updateErrorDisplay();
const result = {
valid: errors.length === 0,
errors
};
this.emit('field:validated', { field: this, result });
return result;
}
// 字段特定验证 - 子类可重写
protected async validateField(value: any): Promise<ValidationError[]> {
return [];
}
// 获取默认值
getDefaultValue(): any {
return this.defaultValue;
}
// 重置字段
async reset(): Promise<void> {
await this.setValue(this.defaultValue);
this.errors = [];
this.updateErrorDisplay();
this.emit('field:reset', { field: this });
}
// 销毁字段
async destroy(): Promise<void> {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
this.emit('field:destroyed', { field: this });
}
// 可见性控制
isVisible(): boolean {
return this.visible;
}
setVisible(visible: boolean): void {
this.visible = visible;
this.updateVisibility();
this.emit('field:visibilityChanged', { field: this, visible });
}
updateVisibility(): void {
if (this.element) {
this.element.style.display = this.visible ? '' : 'none';
}
}
// 禁用状态控制
isDisabled(): boolean {
return this.disabled;
}
setDisabled(disabled: boolean): void {
this.disabled = disabled;
this.updateDisabled();
this.emit('field:disabledChanged', { field: this, disabled });
}
updateDisabled(): void {
if (this.element) {
const input = this.element.querySelector('input, select, textarea') as HTMLInputElement;
if (input) {
input.disabled = this.disabled;
}
}
}
// 只读状态控制
isReadonly(): boolean {
return this.readonly;
}
setReadonly(readonly: boolean): void {
this.readonly = readonly;
this.updateReadonly();
this.emit('field:readonlyChanged', { field: this, readonly });
}
updateReadonly(): void {
if (this.element) {
const input = this.element.querySelector('input, select, textarea') as HTMLInputElement;
if (input) {
input.readOnly = this.readonly;
}
}
}
// 获取依赖字段
getDependencies(): string[] {
const dependencies: string[] = [];
// 从可见性条件中提取依赖
if (this.config.visibleWhen) {
dependencies.push(...this.extractDependenciesFromCondition(this.config.visibleWhen));
}
// 从禁用条件中提取依赖
if (this.config.disabledWhen) {
dependencies.push(...this.extractDependenciesFromCondition(this.config.disabledWhen));
}
// 从选项数据源中提取依赖
if (this.config.optionsSource && this.config.optionsSource.dependencies) {
dependencies.push(...this.config.optionsSource.dependencies);
}
return [...new Set(dependencies)];
}
// 更新选项(用于依赖字段)
async updateOptions(): Promise<void> {
// 子类可重写
}
// 事件处理
on(event: string, handler: Function): void {
this.eventBus.on(event, handler);
}
off(event: string, handler: Function): void {
this.eventBus.off(event, handler);
}
emit(event: string, data: any): void {
this.eventBus.emit(event, data);
}
// 工具方法
protected isEmpty(value: any): boolean {
return value === null || value === undefined || value === '' ||
(Array.isArray(value) && value.length === 0);
}
protected updateErrorDisplay(): void {
if (this.element) {
const errorContainer = this.element.querySelector('.field-errors');
if (errorContainer) {
errorContainer.innerHTML = '';
if (this.errors.length > 0) {
this.errors.forEach(error => {
const errorElement = document.createElement('div');
errorElement.className = 'field-error';
errorElement.textContent = error;
errorContainer.appendChild(errorElement);
});
this.element.classList.add('has-error');
} else {
this.element.classList.remove('has-error');
}
}
}
}
protected extractDependenciesFromCondition(condition: FieldCondition): string[] {
const dependencies: string[] = [];
if (condition.field) {
dependencies.push(condition.field);
}
if (condition.and) {
condition.and.forEach(subCondition => {
dependencies.push(...this.extractDependenciesFromCondition(subCondition));
});
}
if (condition.or) {
condition.or.forEach(subCondition => {
dependencies.push(...this.extractDependenciesFromCondition(subCondition));
});
}
return dependencies;
}
protected createFieldContainer(): HTMLElement {
const container = document.createElement('div');
container.className = `field-container field-${this.type}`;
container.setAttribute('data-field-name', this.name);
// 添加标签
if (this.label) {
const label = document.createElement('label');
label.className = 'field-label';
label.textContent = this.label;
if (this.config.required) {
label.classList.add('required');
}
container.appendChild(label);
}
// 添加字段包装器
const wrapper = document.createElement('div');
wrapper.className = 'field-wrapper';
container.appendChild(wrapper);
// 添加帮助文本
if (this.config.help) {
const help = document.createElement('div');
help.className = 'field-help';
help.textContent = this.config.help;
container.appendChild(help);
}
// 添加错误容器
const errors = document.createElement('div');
errors.className = 'field-errors';
container.appendChild(errors);
return container;
}
}
// 字段选项
interface FieldOptions {
config: FieldConfig;
definition: FieldDefinition;
form: Form;
validationEngine: FormValidationEngine;
}
// 字段配置
interface FieldConfig {
name: string;
type: string;
label?: string;
placeholder?: string;
help?: string;
required?: boolean;
requiredMessage?: string;
defaultValue?: any;
visible?: boolean;
disabled?: boolean;
readonly?: boolean;
rules?: ValidationRule[];
visibleWhen?: FieldCondition;
disabledWhen?: FieldCondition;
optionsSource?: OptionsSource;
[key: string]: any;
}
// 字段条件
interface FieldCondition {
field?: string;
operator?: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith';
value?: any;
and?: FieldCondition[];
or?: FieldCondition[];
}
// 选项数据源
interface OptionsSource {
type: 'static' | 'api' | 'dataSource' | 'function';
data?: any;
url?: string;
dataSourceId?: string;
dependencies?: string[];
transform?: (data: any) => any[];
}
7.3.2 具体字段实现
// 文本字段实现
class TextField extends FormField {
private input: HTMLInputElement | null = null;
async render(container: HTMLElement): Promise<HTMLElement> {
this.element = this.createFieldContainer();
const wrapper = this.element.querySelector('.field-wrapper') as HTMLElement;
// 创建输入框
this.input = document.createElement('input');
this.input.type = 'text';
this.input.className = 'field-input';
this.input.placeholder = this.config.placeholder || '';
if (this.config.maxLength) {
this.input.maxLength = this.config.maxLength;
}
// 绑定事件
this.input.addEventListener('input', () => {
this.value = this.input!.value;
this.emit('value:changed', { field: this, value: this.value });
});
this.input.addEventListener('blur', () => {
this.validate();
this.emit('field:blur', { field: this });
});
wrapper.appendChild(this.input);
// 添加清除按钮
if (this.config.clearable) {
const clearButton = document.createElement('button');
clearButton.type = 'button';
clearButton.className = 'field-clear';
clearButton.innerHTML = '×';
clearButton.addEventListener('click', () => this.clear());
wrapper.appendChild(clearButton);
}
container.appendChild(this.element);
return this.element;
}
getValue(): string {
return this.value || '';
}
async setValue(value: string): Promise<void> {
this.value = value || '';
if (this.input) {
this.input.value = this.value;
}
this.emit('value:changed', { field: this, value: this.value });
}
focus(): void {
if (this.input) {
this.input.focus();
}
}
blur(): void {
if (this.input) {
this.input.blur();
}
}
clear(): void {
this.setValue('');
}
protected async validateField(value: string): Promise<ValidationError[]> {
const errors: ValidationError[] = [];
// 长度验证
if (this.config.minLength && value.length < this.config.minLength) {
errors.push({
field: this.name,
rule: ValidationRuleType.MIN_LENGTH,
message: `${this.label} 最小长度为 ${this.config.minLength}`,
value
});
}
if (this.config.maxLength && value.length > this.config.maxLength) {
errors.push({
field: this.name,
rule: ValidationRuleType.MAX_LENGTH,
message: `${this.label} 最大长度为 ${this.config.maxLength}`,
value
});
}
// 正则验证
if (this.config.pattern) {
const regex = new RegExp(this.config.pattern);
if (!regex.test(value)) {
errors.push({
field: this.name,
rule: ValidationRuleType.PATTERN,
message: this.config.patternMessage || `${this.label} 格式不正确`,
value
});
}
}
return errors;
}
}
// 数字字段实现
class NumberField extends FormField {
private input: HTMLInputElement | null = null;
async render(container: HTMLElement): Promise<HTMLElement> {
this.element = this.createFieldContainer();
const wrapper = this.element.querySelector('.field-wrapper') as HTMLElement;
// 创建输入框
this.input = document.createElement('input');
this.input.type = 'number';
this.input.className = 'field-input';
this.input.placeholder = this.config.placeholder || '';
if (this.config.min !== undefined) {
this.input.min = this.config.min.toString();
}
if (this.config.max !== undefined) {
this.input.max = this.config.max.toString();
}
if (this.config.step !== undefined) {
this.input.step = this.config.step.toString();
}
// 绑定事件
this.input.addEventListener('input', () => {
const numValue = parseFloat(this.input!.value);
this.value = isNaN(numValue) ? null : numValue;
this.emit('value:changed', { field: this, value: this.value });
});
this.input.addEventListener('blur', () => {
this.validate();
this.emit('field:blur', { field: this });
});
wrapper.appendChild(this.input);
container.appendChild(this.element);
return this.element;
}
getValue(): number | null {
return this.value;
}
async setValue(value: number | null): Promise<void> {
this.value = value;
if (this.input) {
this.input.value = value !== null ? value.toString() : '';
}
this.emit('value:changed', { field: this, value: this.value });
}
focus(): void {
if (this.input) {
this.input.focus();
}
}
blur(): void {
if (this.input) {
this.input.blur();
}
}
clear(): void {
this.setValue(null);
}
protected async validateField(value: number): Promise<ValidationError[]> {
const errors: ValidationError[] = [];
// 范围验证
if (this.config.min !== undefined && value < this.config.min) {
errors.push({
field: this.name,
rule: ValidationRuleType.MIN_VALUE,
message: `${this.label} 最小值为 ${this.config.min}`,
value
});
}
if (this.config.max !== undefined && value > this.config.max) {
errors.push({
field: this.name,
rule: ValidationRuleType.MAX_VALUE,
message: `${this.label} 最大值为 ${this.config.max}`,
value
});
}
return errors;
}
}
// 选择字段实现
class SelectField extends FormField {
private select: HTMLSelectElement | null = null;
private options: FieldOption[] = [];
async render(container: HTMLElement): Promise<HTMLElement> {
this.element = this.createFieldContainer();
const wrapper = this.element.querySelector('.field-wrapper') as HTMLElement;
// 创建选择框
this.select = document.createElement('select');
this.select.className = 'field-input';
if (this.config.multiple) {
this.select.multiple = true;
}
// 绑定事件
this.select.addEventListener('change', () => {
this.updateValueFromSelect();
this.emit('value:changed', { field: this, value: this.value });
});
this.select.addEventListener('blur', () => {
this.validate();
this.emit('field:blur', { field: this });
});
wrapper.appendChild(this.select);
// 加载选项
await this.loadOptions();
container.appendChild(this.element);
return this.element;
}
getValue(): any {
return this.value;
}
async setValue(value: any): Promise<void> {
this.value = value;
this.updateSelectFromValue();
this.emit('value:changed', { field: this, value: this.value });
}
focus(): void {
if (this.select) {
this.select.focus();
}
}
blur(): void {
if (this.select) {
this.select.blur();
}
}
clear(): void {
this.setValue(this.config.multiple ? [] : null);
}
async updateOptions(): Promise<void> {
await this.loadOptions();
}
private async loadOptions(): Promise<void> {
if (this.config.options) {
this.options = this.config.options;
} else if (this.config.optionsSource) {
this.options = await this.loadOptionsFromSource();
} else {
this.options = [];
}
this.renderOptions();
}
private async loadOptionsFromSource(): Promise<FieldOption[]> {
const source = this.config.optionsSource!;
switch (source.type) {
case 'static':
return source.data || [];
case 'api':
if (source.url) {
const response = await fetch(source.url);
const data = await response.json();
return source.transform ? source.transform(data) : data;
}
break;
case 'dataSource':
if (source.dataSourceId) {
const result = await this.form['dataEngine'].query(source.dataSourceId);
return source.transform ? source.transform(result.data) : result.data;
}
break;
case 'function':
if (source.transform) {
const formData = this.form.getData();
return source.transform(formData);
}
break;
}
return [];
}
private renderOptions(): void {
if (!this.select) {
return;
}
// 清空现有选项
this.select.innerHTML = '';
// 添加占位符选项
if (this.config.placeholder && !this.config.multiple) {
const placeholderOption = document.createElement('option');
placeholderOption.value = '';
placeholderOption.textContent = this.config.placeholder;
placeholderOption.disabled = true;
placeholderOption.selected = true;
this.select.appendChild(placeholderOption);
}
// 添加选项
this.options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.label;
this.select!.appendChild(optionElement);
});
// 更新选中状态
this.updateSelectFromValue();
}
private updateValueFromSelect(): void {
if (!this.select) {
return;
}
if (this.config.multiple) {
const selectedValues: string[] = [];
for (let i = 0; i < this.select.options.length; i++) {
if (this.select.options[i].selected) {
selectedValues.push(this.select.options[i].value);
}
}
this.value = selectedValues;
} else {
this.value = this.select.value || null;
}
}
private updateSelectFromValue(): void {
if (!this.select) {
return;
}
if (this.config.multiple && Array.isArray(this.value)) {
for (let i = 0; i < this.select.options.length; i++) {
this.select.options[i].selected = this.value.includes(this.select.options[i].value);
}
} else {
this.select.value = this.value || '';
}
}
}
// 字段选项
interface FieldOption {
value: string;
label: string;
disabled?: boolean;
}
// 日期字段实现
class DateField extends FormField {
private input: HTMLInputElement | null = null;
async render(container: HTMLElement): Promise<HTMLElement> {
this.element = this.createFieldContainer();
const wrapper = this.element.querySelector('.field-wrapper') as HTMLElement;
// 创建日期输入框
this.input = document.createElement('input');
this.input.type = 'date';
this.input.className = 'field-input';
// 绑定事件
this.input.addEventListener('change', () => {
this.value = this.input!.value || null;
this.emit('value:changed', { field: this, value: this.value });
});
this.input.addEventListener('blur', () => {
this.validate();
this.emit('field:blur', { field: this });
});
wrapper.appendChild(this.input);
container.appendChild(this.element);
return this.element;
}
getValue(): string | null {
return this.value;
}
async setValue(value: string | null): Promise<void> {
this.value = value;
if (this.input) {
this.input.value = value || '';
}
this.emit('value:changed', { field: this, value: this.value });
}
focus(): void {
if (this.input) {
this.input.focus();
}
}
blur(): void {
if (this.input) {
this.input.blur();
}
}
clear(): void {
this.setValue(null);
}
}
// 文件上传字段实现
class FileField extends FormField {
private input: HTMLInputElement | null = null;
private fileList: HTMLElement | null = null;
private files: File[] = [];
async render(container: HTMLElement): Promise<HTMLElement> {
this.element = this.createFieldContainer();
const wrapper = this.element.querySelector('.field-wrapper') as HTMLElement;
// 创建文件输入框
this.input = document.createElement('input');
this.input.type = 'file';
this.input.className = 'field-input';
if (this.config.accept) {
this.input.accept = this.config.accept;
}
if (this.config.multiple) {
this.input.multiple = true;
}
// 绑定事件
this.input.addEventListener('change', () => {
this.handleFileChange();
});
wrapper.appendChild(this.input);
// 创建文件列表
this.fileList = document.createElement('div');
this.fileList.className = 'file-list';
wrapper.appendChild(this.fileList);
container.appendChild(this.element);
return this.element;
}
getValue(): File[] {
return this.files;
}
async setValue(value: File[]): Promise<void> {
this.files = value || [];
this.updateFileList();
this.emit('value:changed', { field: this, value: this.value });
}
focus(): void {
if (this.input) {
this.input.focus();
}
}
blur(): void {
if (this.input) {
this.input.blur();
}
}
clear(): void {
this.setValue([]);
if (this.input) {
this.input.value = '';
}
}
private handleFileChange(): void {
if (!this.input || !this.input.files) {
return;
}
const newFiles = Array.from(this.input.files);
// 验证文件
const validFiles = newFiles.filter(file => this.validateFile(file));
if (this.config.multiple) {
this.files = [...this.files, ...validFiles];
// 检查最大文件数量
if (this.config.maxCount && this.files.length > this.config.maxCount) {
this.files = this.files.slice(0, this.config.maxCount);
}
} else {
this.files = validFiles.slice(0, 1);
}
this.updateFileList();
this.emit('value:changed', { field: this, value: this.files });
}
private validateFile(file: File): boolean {
// 检查文件大小
if (this.config.maxSize && file.size > this.config.maxSize) {
console.warn(`文件 ${file.name} 超过最大大小限制`);
return false;
}
// 检查文件类型
if (this.config.accept && this.config.accept !== '*') {
const acceptTypes = this.config.accept.split(',').map(type => type.trim());
const fileType = file.type;
const fileName = file.name;
const isAccepted = acceptTypes.some(acceptType => {
if (acceptType.startsWith('.')) {
return fileName.toLowerCase().endsWith(acceptType.toLowerCase());
} else {
return fileType.match(acceptType.replace('*', '.*'));
}
});
if (!isAccepted) {
console.warn(`文件 ${file.name} 类型不被接受`);
return false;
}
}
return true;
}
private updateFileList(): void {
if (!this.fileList) {
return;
}
this.fileList.innerHTML = '';
this.files.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
const fileName = document.createElement('span');
fileName.className = 'file-name';
fileName.textContent = file.name;
const fileSize = document.createElement('span');
fileSize.className = 'file-size';
fileSize.textContent = this.formatFileSize(file.size);
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'file-remove';
removeButton.textContent = '删除';
removeButton.addEventListener('click', () => {
this.removeFile(index);
});
fileItem.appendChild(fileName);
fileItem.appendChild(fileSize);
fileItem.appendChild(removeButton);
this.fileList.appendChild(fileItem);
});
}
private removeFile(index: number): void {
this.files.splice(index, 1);
this.updateFileList();
this.emit('value:changed', { field: this, value: this.files });
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
7.4 表单渲染引擎
7.4.1 表单渲染器
// 表单渲染器
class FormRenderer {
private form: Form;
private container: HTMLElement;
private fieldRegistry: FieldRegistry;
private eventBus: EventBus;
private element: HTMLElement | null = null;
private fieldElements: Map<string, HTMLElement> = new Map();
constructor(options: FormRendererOptions) {
this.form = options.form;
this.container = options.container;
this.fieldRegistry = options.fieldRegistry;
this.eventBus = options.eventBus;
}
async render(): Promise<void> {
// 创建表单元素
this.element = document.createElement('form');
this.element.className = 'lowcode-form';
this.element.setAttribute('data-form-id', this.form.id);
// 阻止默认提交
this.element.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubmit();
});
// 渲染表单标题
if (this.form.title) {
const title = document.createElement('h2');
title.className = 'form-title';
title.textContent = this.form.title;
this.element.appendChild(title);
}
// 渲染表单描述
if (this.form.description) {
const description = document.createElement('p');
description.className = 'form-description';
description.textContent = this.form.description;
this.element.appendChild(description);
}
// 渲染字段
await this.renderFields();
// 渲染表单按钮
this.renderFormActions();
// 添加到容器
this.container.appendChild(this.element);
// 绑定事件
this.bindEvents();
}
async destroy(): Promise<void> {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.fieldElements.clear();
this.element = null;
}
private async renderFields(): Promise<void> {
const fieldsContainer = document.createElement('div');
fieldsContainer.className = 'form-fields';
const fields = this.form.getFields();
for (const field of fields) {
const fieldElement = await field.render(fieldsContainer);
this.fieldElements.set(field.name, fieldElement);
}
this.element!.appendChild(fieldsContainer);
}
private renderFormActions(): void {
const actionsContainer = document.createElement('div');
actionsContainer.className = 'form-actions';
// 提交按钮
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.className = 'btn btn-primary';
submitButton.textContent = '提交';
actionsContainer.appendChild(submitButton);
// 重置按钮
const resetButton = document.createElement('button');
resetButton.type = 'button';
resetButton.className = 'btn btn-secondary';
resetButton.textContent = '重置';
resetButton.addEventListener('click', () => this.handleReset());
actionsContainer.appendChild(resetButton);
this.element!.appendChild(actionsContainer);
}
private bindEvents(): void {
// 监听表单事件
this.form.on('form:dataChanged', () => {
this.updateFieldVisibility();
});
this.form.on('form:validated', (event) => {
this.updateValidationDisplay(event.result);
});
}
private async handleSubmit(): Promise<void> {
try {
const result = await this.form.submit();
if (result.success) {
this.eventBus.emit('form:submitSuccess', { form: this.form, result });
} else {
this.eventBus.emit('form:submitError', { form: this.form, result });
}
} catch (error) {
this.eventBus.emit('form:submitError', { form: this.form, error });
}
}
private async handleReset(): Promise<void> {
await this.form.reset();
this.eventBus.emit('form:reset', { form: this.form });
}
private updateFieldVisibility(): void {
const fields = this.form.getFields();
fields.forEach(field => {
const element = this.fieldElements.get(field.name);
if (element) {
element.style.display = field.isVisible() ? '' : 'none';
}
});
}
private updateValidationDisplay(result: ValidationResult): void {
// 清除之前的验证状态
if (this.element) {
this.element.classList.remove('has-errors');
if (!result.valid) {
this.element.classList.add('has-errors');
}
}
}
}
// 表单渲染器选项
interface FormRendererOptions {
form: Form;
container: HTMLElement;
fieldRegistry: FieldRegistry;
eventBus: EventBus;
}
7.4.2 表单布局引擎
// 布局类型
enum LayoutType {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
GRID = 'grid',
TABS = 'tabs',
ACCORDION = 'accordion'
}
// 布局配置
interface LayoutConfig {
type: LayoutType;
columns?: number;
gap?: string;
responsive?: boolean;
breakpoints?: {
xs?: number;
sm?: number;
md?: number;
lg?: number;
xl?: number;
};
}
// 表单布局引擎
class FormLayoutEngine {
private form: Form;
private config: LayoutConfig;
private container: HTMLElement;
constructor(form: Form, config: LayoutConfig, container: HTMLElement) {
this.form = form;
this.config = config;
this.container = container;
}
render(): HTMLElement {
switch (this.config.type) {
case LayoutType.VERTICAL:
return this.renderVerticalLayout();
case LayoutType.HORIZONTAL:
return this.renderHorizontalLayout();
case LayoutType.GRID:
return this.renderGridLayout();
case LayoutType.TABS:
return this.renderTabsLayout();
case LayoutType.ACCORDION:
return this.renderAccordionLayout();
default:
return this.renderVerticalLayout();
}
}
private renderVerticalLayout(): HTMLElement {
const layout = document.createElement('div');
layout.className = 'form-layout form-layout-vertical';
if (this.config.gap) {
layout.style.gap = this.config.gap;
}
return layout;
}
private renderHorizontalLayout(): HTMLElement {
const layout = document.createElement('div');
layout.className = 'form-layout form-layout-horizontal';
layout.style.display = 'flex';
layout.style.flexDirection = 'row';
layout.style.flexWrap = 'wrap';
if (this.config.gap) {
layout.style.gap = this.config.gap;
}
return layout;
}
private renderGridLayout(): HTMLElement {
const layout = document.createElement('div');
layout.className = 'form-layout form-layout-grid';
layout.style.display = 'grid';
if (this.config.columns) {
layout.style.gridTemplateColumns = `repeat(${this.config.columns}, 1fr)`;
}
if (this.config.gap) {
layout.style.gap = this.config.gap;
}
// 响应式处理
if (this.config.responsive && this.config.breakpoints) {
this.applyResponsiveGrid(layout);
}
return layout;
}
private renderTabsLayout(): HTMLElement {
const layout = document.createElement('div');
layout.className = 'form-layout form-layout-tabs';
// 创建标签页头部
const tabsHeader = document.createElement('div');
tabsHeader.className = 'tabs-header';
// 创建标签页内容
const tabsContent = document.createElement('div');
tabsContent.className = 'tabs-content';
layout.appendChild(tabsHeader);
layout.appendChild(tabsContent);
return layout;
}
private renderAccordionLayout(): HTMLElement {
const layout = document.createElement('div');
layout.className = 'form-layout form-layout-accordion';
return layout;
}
private applyResponsiveGrid(layout: HTMLElement): void {
const breakpoints = this.config.breakpoints!;
// 创建媒体查询样式
const style = document.createElement('style');
let css = '';
if (breakpoints.xs) {
css += `@media (max-width: 576px) {
.form-layout-grid {
grid-template-columns: repeat(${breakpoints.xs}, 1fr);
}
}`;
}
if (breakpoints.sm) {
css += `@media (min-width: 576px) and (max-width: 768px) {
.form-layout-grid {
grid-template-columns: repeat(${breakpoints.sm}, 1fr);
}
}`;
}
if (breakpoints.md) {
css += `@media (min-width: 768px) and (max-width: 992px) {
.form-layout-grid {
grid-template-columns: repeat(${breakpoints.md}, 1fr);
}
}`;
}
if (breakpoints.lg) {
css += `@media (min-width: 992px) and (max-width: 1200px) {
.form-layout-grid {
grid-template-columns: repeat(${breakpoints.lg}, 1fr);
}
}`;
}
if (breakpoints.xl) {
css += `@media (min-width: 1200px) {
.form-layout-grid {
grid-template-columns: repeat(${breakpoints.xl}, 1fr);
}
}`;
}
style.textContent = css;
document.head.appendChild(style);
}
}
7.5 表单验证引擎
7.5.1 验证规则系统
”`typescript // 验证规则类型 enum ValidationRuleType { REQUIRED = ‘required’, MIN_LENGTH = ‘minLength’, MAX_LENGTH = ‘maxLength’, PATTERN = ‘pattern’, EMAIL = ‘email’, URL = ‘url’, NUMBER = ‘number’, INTEGER = ‘integer’, MIN = ‘min’, MAX = ‘max’, CUSTOM = ‘custom’ }
// 验证规则
interface ValidationRule {
type: ValidationRuleType;
value?: any;
message?: string;
validator?: (value: any, field: FormField) => boolean | Promise
// 验证结果 interface ValidationResult { valid: boolean; errors: ValidationError[]; }
// 验证错误 interface ValidationError { field: string; rule: ValidationRuleType; message: string; value?: any; }
// 表单验证引擎
class FormValidationEngine {
private form: Form;
private rules: Map
constructor(form: Form) { this.form = form; this.initializeBuiltInValidators(); }
// 添加验证规则 addRule(fieldName: string, rule: ValidationRule): void { if (!this.rules.has(fieldName)) { this.rules.set(fieldName, []); }
this.rules.get(fieldName)!.push(rule);
}
// 移除验证规则 removeRule(fieldName: string, ruleType: ValidationRuleType): void { const fieldRules = this.rules.get(fieldName); if (fieldRules) { const index = fieldRules.findIndex(rule => rule.type === ruleType); if (index !== -1) { fieldRules.splice(index, 1); } } }
// 清除字段的所有验证规则 clearRules(fieldName: string): void { this.rules.delete(fieldName); }
// 注册自定义验证器 registerValidator(name: string, validator: Function): void { this.customValidators.set(name, validator); }
// 验证单个字段
async validateField(fieldName: string): Promise
const rules = this.rules.get(fieldName) || [];
const errors: ValidationError[] = [];
for (const rule of rules) {
const isValid = await this.validateRule(field, rule);
if (!isValid) {
errors.push({
field: fieldName,
rule: rule.type,
message: rule.message || this.getDefaultMessage(rule.type, rule.value),
value: field.getValue()
});
}
}
return {
valid: errors.length === 0,
errors
};
}
// 验证整个表单
async validateForm(): Promise
for (const field of fields) {
const result = await this.validateField(field.name);
allErrors.push(...result.errors);
}
return {
valid: allErrors.length === 0,
errors: allErrors
};
}
// 验证单个规则
private async validateRule(field: FormField, rule: ValidationRule): Promise
switch (rule.type) {
case ValidationRuleType.REQUIRED:
return this.validateRequired(value);
case ValidationRuleType.MIN_LENGTH:
return this.validateMinLength(value, rule.value);
case ValidationRuleType.MAX_LENGTH:
return this.validateMaxLength(value, rule.value);
case ValidationRuleType.PATTERN:
return this.validatePattern(value, rule.value);
case ValidationRuleType.EMAIL:
return this.validateEmail(value);
case ValidationRuleType.URL:
return this.validateUrl(value);
case ValidationRuleType.NUMBER:
return this.validateNumber(value);
case ValidationRuleType.INTEGER:
return this.validateInteger(value);
case ValidationRuleType.MIN:
return this.validateMin(value, rule.value);
case ValidationRuleType.MAX:
return this.validateMax(value, rule.value);
case ValidationRuleType.CUSTOM:
if (rule.validator) {
return await rule.validator(value, field);
}
return true;
default:
return true;
}
}
// 内置验证器 private initializeBuiltInValidators(): void { // Required 验证器 this.customValidators.set(‘required’, (value: any) => { return this.validateRequired(value); });
// Email 验证器
this.customValidators.set('email', (value: any) => {
return this.validateEmail(value);
});
// URL 验证器
this.customValidators.set('url', (value: any) => {
return this.validateUrl(value);
});
// 手机号验证器
this.customValidators.set('phone', (value: any) => {
if (!value) return true;
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(value.toString());
});
// 身份证验证器
this.customValidators.set('idCard', (value: any) => {
if (!value) return true;
const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return idCardRegex.test(value.toString());
});
}
private validateRequired(value: any): boolean { if (value === null || value === undefined) { return false; }
if (typeof value === 'string') {
return value.trim().length > 0;
}
if (Array.isArray(value)) {
return value.length > 0;
}
return true;
}
private validateMinLength(value: any, minLength: number): boolean { if (!value) return true;
if (typeof value === 'string' || Array.isArray(value)) {
return value.length >= minLength;
}
return true;
}
private validateMaxLength(value: any, maxLength: number): boolean { if (!value) return true;
if (typeof value === 'string' || Array.isArray(value)) {
return value.length <= maxLength;
}
return true;
}
private validatePattern(value: any, pattern: string | RegExp): boolean { if (!value) return true;
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
return regex.test(value.toString());
}
private validateEmail(value: any): boolean { if (!value) return true;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value.toString());
}
private validateUrl(value: any): boolean { if (!value) return true;
try {
new URL(value.toString());
return true;
} catch {
return false;
}
}
private validateNumber(value: any): boolean { if (!value) return true;
return !isNaN(Number(value));
}
private validateInteger(value: any): boolean { if (!value) return true;
const num = Number(value);
return !isNaN(num) && Number.isInteger(num);
}
private validateMin(value: any, min: number): boolean { if (!value) return true;
const num = Number(value);
return !isNaN(num) && num >= min;
}
private validateMax(value: any, max: number): boolean { if (!value) return true;
const num = Number(value);
return !isNaN(num) && num <= max;
}
private getDefaultMessage(ruleType: ValidationRuleType, value?: any): string {
switch (ruleType) {
case ValidationRuleType.REQUIRED:
return ‘此字段为必填项’;
case ValidationRuleType.MIN_LENGTH:
return 最少需要 ${value} 个字符
;
case ValidationRuleType.MAX_LENGTH:
return 最多允许 ${value} 个字符
;
case ValidationRuleType.EMAIL:
return ‘请输入有效的邮箱地址’;
case ValidationRuleType.URL:
return ‘请输入有效的URL地址’;
case ValidationRuleType.NUMBER:
return ‘请输入有效的数字’;
case ValidationRuleType.INTEGER:
return ‘请输入有效的整数’;
case ValidationRuleType.MIN:
return 值不能小于 ${value}
;
case ValidationRuleType.MAX:
return 值不能大于 ${value}
;
default:
return ‘输入值无效’;
}
}
}