angular schema form 数据区动表单
- 需求分析
根据给定的schema数据来生成移动端的数据表单。刚想到这个需求,我也没啥思路!先做一个简单的,一步一步来,总会实现的!
需求简化
我们实现一个动态生成组件的功能,简化到这一步,我想到了上一篇文章10分钟快速上手angular cdk,提到cdk里面有一个portal可以实现,既然有了思路那就动手吧!
<ng-container *ngFor="let item of list">
<ng-container [cdkPortalOutlet]="item"></ng-container>
</ng-container>
import { Component, OnInit } from '@angular/core';
import { FieldInputComponent } from 'iwe7/form/src/field-input/field-input.component';
import { ComponentPortal } from '@angular/cdk/portal';
@Component({
selector: 'form-container',
templateUrl: './form-container.component.html',
styleUrls: ['./form-container.component.scss']
})
export class FormContainerComponent implements OnInit {
list: any[] = [];
constructor() {}
ngOnInit() {
const inputPortal = new ComponentPortal(FieldInputComponent);
this.list.push(inputPortal);
}
}
这样我们就以最简单的方式生成了一个input表单
继续深化-动态创建组件
第一个小目标我们已经实现了,下面接着深度优化。这也是拿到一个需求的正常思路,先做着!
说真的,我也是一面写文章一面整理思路,因为我发现有的时候,写出来的东西思路会很清晰,就想以前喜欢蹲在厕所里敲代码脑子转的很快一样!
import { Component, OnInit } from '@angular/core';
import { FieldRegisterService } from 'iwe7/form/src/field-register.service';
@Component({
selector: 'form-container',
templateUrl: './form-container.component.html',
styleUrls: ['./form-container.component.scss']
})
export class FormContainerComponent implements OnInit {
list: any[] = [
{
type: 'input'
}
];
constructor(public register: FieldRegisterService) {}
ngOnInit() {
// 这里集成了一个服务,用来提供Portal
this.list.map(res => {
res.portal = this.register.getComponentPortal(res.type);
});
}
}
服务实现
import { Injectable, InjectionToken, Type, Injector } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import { FieldInputComponent } from './field-input/field-input.component';
export interface FormFieldData {
type: string;
component: Type<any>;
}
export const FORM_FIELD_LIBRARY = new InjectionToken<
Map<string, FormFieldData>
>('FormFieldLibrary', {
providedIn: 'root',
factory: () => {
const map = new Map();
map.set('input', {
type: 'input',
component: FieldInputComponent
});
return map;
}
});
@Injectable()
export class FieldRegisterService {
constructor(public injector: Injector) {}
// 通过key索引,得到一个portal
getComponentPortal(key: string) {
const libs = this.injector.get(FORM_FIELD_LIBRARY);
const component = libs.get(key).component;
return new ComponentPortal(component);
}
}
继续深化-发现问题,重新整理思路
这样我们就通过一个给定的list = [{type: 'input'}] 来动态生成一个组件
接下来,我们继续完善这个input,给他加上name[表单提交时的key],placeholder[输入提醒],label[标题],value[默认之],并正确显示!
这个时候我们发现,portal没有提供传递input数据的地方!那只有换方案了,看来他只适合简单的动态生成模板。下面我们自己封装一个directive用于生成组件。
@Directive({
selector: '[createComponent],[createComponentProps]'
})
export class CreateComponentDirective
implements OnInit, AfterViewInit, OnChanges {
@Input() createComponent: string;
// 输入即传入进来的json
@Input() createComponentProps: any;
componentInstance: any;
constructor(
public register: FieldRegisterService,
public view: ViewContainerRef
) {}
ngOnInit() {}
// 当输入变化时,重新生成组件
ngOnChanges(changes: SimpleChanges) {
if ('createComponent' in changes) {
this.create();
}
if ('createComponentProps' in changes) {
this.setProps();
}
}
setProps() {
if (!!this.componentInstance) {
this.componentInstance.props = this.createComponentProps;
this.componentInstance.updateValue();
}
}
create() {
// 清理试图
this.view.clear();
// 创建并插入component
const component = this.register.getComponent(this.createComponent);
const elInjector = this.view.parentInjector;
const componentFactoryResolver = elInjector.get(ComponentFactoryResolver);
const componentFactory = componentFactoryResolver.resolveComponentFactory(
component
);
const componentRef = this.view.createComponent(componentFactory);
// 保存一下,方便后面使用
this.componentInstance = componentRef.instance;
this.setProps();
}
}
- 改造之前的代码
<ng-container *ngFor="let item of list">
<ng-container *createComponent="item.type;props item;"></ng-container>
</ng-container>
export class FormContainerComponent implements OnInit {
list: any[] = [
{
type: 'input',
name: 'realname',
label: '姓名',
placeholder: '请输入姓名',
value: ''
}
];
constructor() {}
ngOnInit() {}
}
改造后的注册器
import {
Injectable,
InjectionToken,
Type,
Injector,
ViewContainerRef,
NgModuleRef,
ComponentFactoryResolver
} from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import { FieldInputComponent } from './field-input/field-input.component';
export interface FormFieldData {
type: string;
component: Type<any>;
}
export const FORM_FIELD_LIBRARY = new InjectionToken<
Map<string, FormFieldData>
>('FormFieldLibrary', {
providedIn: 'root',
factory: () => {
const map = new Map();
map.set('input', {
type: 'input',
component: FieldInputComponent
});
return map;
}
});
@Injectable()
export class FieldRegisterService {
constructor(
public injector: Injector,
private moduleRef: NgModuleRef<any>
) {}
// 通过key索引,得到一个portal
getComponent(key: string) {
const libs = this.injector.get(FORM_FIELD_LIBRARY);
const component = libs.get(key).component;
return component;
}
}
- Input组件
export class FieldInputComponent implements OnInit, OnChanges {
label: string = 'label';
name: string = 'name';
value: string = '';
placeholder: string = 'placeholder';
id: any;
@Input() props: any;
constructor(public injector: Injector) {
this.id = new Date().getTime();
}
ngOnChanges(changes: SimpleChanges) {
if ('props' in changes) {
this.updateValue();
}
}
// 更新配置项目
updateValue() {
const { label, name, value, placeholder } = this.props;
this.label = label || this.label;
this.name = name || this.name;
this.value = value || this.value;
this.placeholder = placeholder || this.placeholder;
}
ngOnInit() {}
}
继续深化-加入表单验证
到目前位置我们已经实现了基础功能,根据传入进来的schema成功创建了一个仅有input的表单。
下面我们继续深化,加上表单验证
- 表单验证逻辑
export class FormValidators {
static required(value): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const result = Validators.required(control);
if (!result) {
return null;
}
return {
...value,
...result
};
};
}
static maxLength(value): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const result = Validators.maxLength(value.limit)(control);
if (!result) {
return null;
}
return {
...value,
...result
};
};
}
static minLength(value): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const result = Validators.minLength(value.limit)(control);
if (!result) {
return null;
}
return {
...value,
...result
};
};
}
}
@Injectable()
export class ValidatorsHelper {
getValidator(key: string): ValidatorFn {
return FormValidators[key];
}
}
- html
<label [attr.for]="'input_'+id" [formGroup]="form">
{{label}}
<input [formControlName]="name" [attr.id]="'input_'+id" #input [attr.name]="name" [attr.value]="value" [attr.placeholder]="placeholder"
/>
<div *ngIf="!form.get(name).valid">{{form.get(name).errors.msg}}</div>
</label>
export class FieldInputComponent implements OnInit, OnChanges {
label: string = 'label';
name: string = 'name';
value: string = '';
placeholder: string = 'placeholder';
validators: any = {};
id: any;
@Input() props: any;
form: FormGroup;
control: AbstractControl;
@ViewChild('input') input: ElementRef;
constructor(
public injector: Injector,
public fb: FormBuilder,
public validatorsHelper: ValidatorsHelper
) {
this.id = new Date().getTime();
// 创建动态表单
this.form = this.fb.group({});
}
ngOnChanges(changes: SimpleChanges) {
if ('props' in changes) {
this.updateValue();
}
}
// 更新配置项目
updateValue() {
const { label, name, value, placeholder, validators } = this.props;
this.label = label || this.label;
this.name = name || this.name;
this.value = value || this.value;
this.placeholder = placeholder || this.placeholder;
this.validators = validators || this.validators;
}
ngOnInit() {
this.control = new FormControl(this.value, {
validators: [],
updateOn: 'blur'
});
this.control.clearValidators();
const validators = [];
Object.keys(this.validators).map(key => {
const value = this.validators[key];
const validator = this.validatorsHelper.getValidator(key);
if (key === 'required') {
validators.push(validator(value));
} else {
validators.push(validator(value));
}
});
this.control.setValidators(validators);
this.form.addControl(this.name, this.control);
// 监听变化
this.form.valueChanges.subscribe(res => {
console.log(res);
});
}
}
list: any[] = [
{
type: 'input',
name: 'realname',
label: '姓名',
placeholder: '请输入姓名',
value: '',
validators: {
required: {
limit: true,
msg: '请输入您的姓名'
},
minLength: {
limit: 3,
msg: '最小长度为3'
},
maxLength: {
limit: 10,
msg: '最大长度为10'
}
}
},
{
type: 'input',
name: 'nickname',
label: '昵称',
placeholder: '请输入昵称',
value: '',
validators: {
required: {
limit: true,
msg: '请输入您的昵称'
},
minLength: {
limit: 3,
msg: '昵称最小长度为3'
},
maxLength: {
limit: 10,
msg: '昵称最大长度为10'
}
}
}
];
小结
目前位置我们已经实现了全部的功能,下面进一步规范后面的开发流程,编写相应的约束。
为后期扩展做准备
import { Input } from '@angular/core';
// 组件设置规范
export abstract class FieldBase {
// 传进来的json数据
@Input() props: { [key: string]: string };
// 更新属性的值
abstract updateValue(): void;
}
// json数据格式规范
export interface SchemaInterface {
type?: string;
name?: string;
label?: string;
placeholder?: string;
value?: string;
validators: {
[key: string]: {
limit: string;
msg: string;
};
};
}
// 表单规范
export interface SchemasInterface {
// 提交的url
url?: string;
// 提交成功
success: {
// 提交成功提醒
msg?: string;
// 提交成功跳转
url?: string;
};
// 提交失败
fail: {
// 提交失败提醒
msg?: string;
url?: string;
};
// 表单设置
fields: SchemaInterface[];
}