美文网首页angularAngular开发指南我爱编程
一步步封装完善一个数据驱动型-表单模型

一步步封装完善一个数据驱动型-表单模型

作者: 杨明明abc | 来源:发表于2018-04-10 00:18 被阅读16次

    angular schema form 数据区动表单

    项目演示地址

    项目github地址

    • 需求分析

    根据给定的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[];
    }
    

    希望有更多的人参与这个项目!一起开发,一起讨论,一起进步!!

    项目演示地址

    项目github地址

    相关文章

      网友评论

      本文标题:一步步封装完善一个数据驱动型-表单模型

      本文链接:https://www.haomeiwen.com/subject/hwowhftx.html