angular2 学习笔记 ( Form 表单 )

更新 : 2019-05-25

disabled 的 control 不会被纳入 form.valid 和 form.value 里, 这个和 html5 行为是一致的. 

https://github.com/angular/angular/issues/11432   

 

更新 : 2018-02-13 

valueChanges and rxjs for filter first value 
需求是这样的 
let fc = new FormControl('');   
fc.valueChanges.subscribe(v => console.log(v));
fc.setValue(''); // '' to '' 没有必要触发
fc.setValue('a'); // '' to a 触发 

可是结果 2 个都触发了.

那这样试试看 : 

fc.valueChanges.pipe(distinctUntilChanged()).subscribe(v => console.log(v));

结果还是一样. 

问题在哪里呢 ? 

首先 ng 的 formControl 看上去想是 BehaviorSubject 因为它有 default 值, 但是行为却像是 Subject. 因为 

let fc = new FormControl('dada');   
fc.valueChanges.subscribe(v => console.log(v)); //并没有触发

虽然之前的代码问题出在, 没有初始值, 所以 distinctUntilChanged 就发挥不了作用了 

我们需要用 startWith 告诉它初始值

let fc = new FormControl('dada');
fc.valueChanges.pipe(startWith('dada'), distinctUntilChanged(), skip(1)).subscribe(v => console.log(v));
fc.setValue('dada'); // 不触发
fc.setValue('dada1'); //触发了

startWith 会马上输入一个值, 然后流入 distinct, distinct 会把值对比上一个(目前没有上一个), 然后记入这一个, 在把值流入 skip(1), 因为我们不要触发初始值, 所以使用了 skip, 如果没有 skip 这时 subscribe 会触发. (startWith 会触发 subscribe)

这样之后的值流入(不经过 startWith 了, startWith 只用一次罢了), distinc 就会和初始值对比就是我们要的结果了. 

如果要在加上一个 debounceTime, 我们必须加在最 startWith 之前. 

pipe(debounceTime(200), startWith(''), distinctUntilChanged(), skip(1))

一旦 subscribe startWith 输入值 -> distinct -> skip 

然后 setValue -> debounce -> distinc -> 触发 ( startWith 只在第一次有用, skip(1) 也是因为已经 skip 掉第一次了)

 

 

 

更新 : 2018-02-10

form.value 坑

let ff = new FormGroup({ 
    name : new FormControl('')
});
ff.get('name')!.valueChanges.subscribe(v => {
  console.log(v); // 'dada'
  console.log(ff.value); // { name : '' } 这时还没有更新到哦 
  console.log(ff.getRawValue())  // { name : 'dada' }
});
ff.get('name')!.setValue('dada');
console.log(ff.value); // { name : 'dada' }

 

 

更新 : 2017-10-19 

this.formControl.setValidators(null);
this.formControl.updateValueAndValidity();

reset validators 后记得调用从新验证哦,Ng 不会帮你做的.

 

更新 : 2017-10-18 

formControl 的监听与广播 

两种监听方式 

1. control.valueChanges.subscribe(v)  
2. control.registerOnChange((value, emitViewToModelChange)
通常我们是想监听来自 view 的更新, 当 accessor.publishMethod(v) 的时候, 上面第一种会被广播, 第二种则收不到. 所以想监听 view -> model 使用第一种 
那么如果我们要监听来自 control.setValue 的话, model -> view or just model change, 我们使用第 2 种, 
setvalue 允许我们广播时声明要不要 让第一种和第二种触发
emitEvent = false 第一种不触发
emitModelToViewChange = false 第 2 种不触发 
emitViewToModelChange = false 第 2 种触发, 然后第二个参数是 就是 emitViewToModelChange 
对了,虽然两种叫 changes 但是值一样也是会触发的,如果不想重复值触发的话,自己写过滤呗.
总结: 
在做 view accessor 时, 我们监听 formControl model to view 所以使用 registerOnChange
// view accessor 
this.viewValue = this.formControl.value; // first time
this.formControl.registerOnChange((v, isViewToModel) => { // model to view
  console.log('should be false', isViewToModel);
  this.viewValue = v;
});

然后通过 formControl view to model 更新

viewToModel(value: any) {
  this.formControl.setValue(value, {
    emitEvent: true,
    emitModelToViewChange: false,
    emitViewToModelChange: true
  });
}

然后呢在外部,我们使用 valueChanges 监听 view to model 的变化

this.formControl.valueChanges.subscribe(v => console.log('view to model', v)); // view to model

再然后呢, 使用 setValue model to view 

modelToView(value: any) {
  this.formControl.setValue(value, {
    emitEvent: false,
    emitModelToViewChange: true,
    emitViewToModelChange: false
  });
}

 最关键的是在做 view accessor 时, 不要依赖 valueChanges 应该只使用 registerOnChange, 这好比你实现 angular ControlvalueAccessor 的时候,我们只依赖 writeValue 去修改 view.

对于 model to view 的时候是否允许 emitEvent 完全可以看你自己有没有对其依赖,但 view accessor 肯定是不依赖的,所以即使 emitEvent false, model to view 依然把 view 处理的很好才对。

 

 

更新 : 2017-08-06 

formControlName and [formControl] 的注入

 <form [formGroup]="form">
  <div formGroupName="obj">
    <input formControlName="name" type="text">
    <input sURLTitle="name" formControlName="URLTitle" type="text">
  </div>
</form> 

<form [formGroup]="form">
  <div [formGroup]="form.get('obj')">
    <input [formControl]="form.get('obj.name')" type="text">
    <input [sURLTitle]="form.get('obj.name')" [formControl]="form.get('obj.URLTitle')" type="text">
  </div>
</form>

这 2 种写法出来的结果是一样的. 

如果我们的指令是 sURLTitle

那么在 sURLTitle 可以通过注入获取到 formControl & formGroup

@Directive({
  selector: '[sURLTitle]'
})
export class URLTitleDirective implements OnInit, OnDestroy {

  constructor(
    // 注意 : 不雅直接注入 FormGroupDirective | FormGroupName, 注入 ControlContainer 才对.
    // @Optional() private formGroupDirective: FormGroupDirective,
    // @Optional() private formGroupName: FormGroupName,
    private closestControl: ControlContainer, // 通过抽象的 ControlContainer 可以获取到上一层 formGroup
    @Optional() private formControlDirective: FormControlDirective,
    @Optional() private FormControlName: FormControlName,
  ) { }

  @Input('sURLTitle')
  URLTitle: string | FormControl

  private sub: ISubscription
  ngOnInit() {
    let watchControl = (typeof (this.URLTitle) === 'string') ? this.closestControl.control.get(this.URLTitle) as FormControl : this.URLTitle;
    let sub = watchControl.valueChanges.subscribe(v => {
      (this.formControlDirective || this.FormControlName).control.setValue(s.toURLTitle(v));
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}

 

 

 

 

 

更新 : 2017-04-21 

form 是不能嵌套的, 但是 formGroup / formGroupDirective 可以 

submit button 只对 <form> 有效果. 如果是 <div [formGroup] > 的话需要自己写 trigger 

child form submit 并不会让 parent form submit, 分的很开. 

 

 

更新 : 2017-03-22

小提示 : 

我们最好把表单里面的 button 都写上 type.

因为 ng 会依据 type 来做处理. 比如 reset 

要注意的是, 如果你不写 type, 默认是 type="submit".

<form [formGroup]="form" #formComponent >
    <button type="button" ></button>
    <button type="submit" ></button>
    <button type="reset" ></button>
</form>

另外, ng 把 formGroup 指令和 formGroup 对象区分的很明显,我们可不要搞混哦. 

上面 formComponent 是有 submitted 属性的, form 则没有 

formComponent.reset() 会把 submitted set to false, form.reset() 则不会. 

formComponent.reset() 会间接调用 form.reset(), 所以数据会清空.

<button type="reset"> 虽然方便不过不支持 window.confirm

我们要自己实现 reset 的话,就必须使用 @Viewchild 来注入 formGroup 指令. 

 

 

2016-08-30

refer : 

 

ng2 的表单和 ng1 类似, 也是用 control 概念来做操作, 当然也有一些地方不同 

最大的特点是它把表单区分为 template drive and model drive 

template drive 和 ng1 很像, 就是通过指令来创建表单的 control 来加以操作. 

model drive 则是直接在 component 内生成 control 然后再绑定到模板上去. 

template drive 的好处是写得少,简单, 适合用于简单的表单

简单的定义是 :

-没有使用 FormArray,

-没有 async valid,

-没有 dynamic | condition validation  

-总之就是要很简单很静态就对了啦.

当然如果你打算自己写各做复杂指令去让 template drive 无所不能, 也是可以办到的. 有心铁棒磨成针嘛.. 你爱磨就去啦..

model drive 的好处就是方便写测试, 不需要依赖 view. 

 

模板驱动 (template drive):

<form novalidate #form="ngForm" (ngSubmit)="submit(form)">
       
</form>

没能嵌套表单了哦! 

通过 #form="ngForm" 我们可以获取到 ngForm 指令, 并且操作它, 比如 form.valid, form.value 等 

ngSubmit 配合 button type=submit 使用 

<input type="text" placeholder="name"
        [(ngModel)]="person.name" name="name" #name="ngModel" required minlength="5" maxlength="10" />
<p>name ok : {{ name.valid }}</p>  

[(ngModel)] 实现双向绑定和 get set value for input 

name="name" 实现 create control to form 

#name 引用 ngModel 指令,可以获取 name.valid 等 

required, minlength, maxlength 是原生提供的验证. ng2 给的原生验证指令很少,连 email,number 都没有哦. 

<fieldset ngModelGroup="address">
    <legend>Address</legend>
    <div>
        Street: <input type="text" [(ngModel)]="person.address.country" name="country" #country="ngModel" required />
    </div> 
    <div>country ok : {{ country.valid }}</div>
</fieldset>

如果值是对象的话,请开多一个 ngModelGroup, 这有点像表单嵌套了.

<div>
    <div *ngFor="let data of person.datas; let i=index;">
        <div ngModelGroup="{{ 'datas['+ i +'].data' }}">
            <input type="text" [(ngModel)]="data.key" name="key" #key="ngModel" required />
        </div>
    </div>
</div>  

遇到数组的话,建议不适用 template drive, 改用 model drive 比较适合. 上面这个有点牵强...

 

 

在自定义组件上使用 ngModel 

ng1 是通过 require ngModelControl 来实现 

ng2 有点不同 

@Component({ 
    selector: "my-input",
    template: `
        <input type="text" [(ngModel)]="value"  />
    `,
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => MyInputComponent),
        multi: true
    }]
})

首先写一个 provide 扩展 NG_VALUE_ACCESSOR 让 ng 认识它 .

export class MyInputComponent implements OnInit, ControlValueAccessor {}

实现 ControlValueAccessor 接口 

//outside to inside     
writeValue(outsideValue: any): void {
    this._value = outsideValue;
};

//inside to outside
//注册一个方法, 当 inside value updated then need call it : fn(newValue)
registerOnChange(fn: (newValue : any) => void): void {
    this.publichValue = fn;
}

//inside to outside
registerOnTouched(fn: any): void {
    this.publichTouched = fn;
}  

主要 3 个方法 

writeValue 是当外部数据修改时被调用来更新内部的。 

registerOnChange(fn) 把这个 fn 注册到内部方法上, 当内部值更新时调用它 this.publishValue(newValue); 

registerOnTouched(fn) 也是一样注册然后调用当 touched 

使用时这样的 : 

<my-input [(ngModel)]="person.like" name="like" email #like="ngModel" ></my-input>
value : {{ like.value }}

执行的顺序是 ngOnInit-> writeValue-> registerOnChange -> registerOnTouched -> ngAfterContentInit -> ngAfterViewInit

如果内部也是使用 formControl 来维护 value 的话, 通常在写入时我们可以关掉 emitEvent, 不然又触发 onChange 去 publish value (但即使你这样做,也不会造成死循环 error 哦, ng 好厉害呢)

writeValue(value: any): void { 
    this.formControl.setValue(value,{ emitEvent : false });        
};

 

Model drive

自定义 validator 指令 

angular 只提供了 4 种 validation : required, minlength, maxlength, pattern

好吝啬 ! 

class MyValidators {
    static email(value: string): ValidatorFn {
        return function (c: AbstractControl) {            
            return (c.value == value) ? null : { "email": false };            
        };
    }  
}

this.registerForm = this.formBuilder.group({
    firstname: ['', MyValidators.email("abc")] 
}); 

如果验证通过返回 null, 如果失败返回一个对象 { email : false }; 

还有 async 的,不过我们有找到比较可靠的教程,以后才讲吧.

上面这个是 model drive 的,如果你希望支持 template drive 可以参考这个 : 

http://blog.thoughtram.io/angular/2016/03/14/custom-validators-in-angular-2.html

 

在大部分情况下, model drive 是更好的选择, 因为它把逻辑才开了, 不要依赖模板是 angular2 一个很重要的思想, 我们要尽量少依赖模板来写业务逻辑, 因为在多设备开发情况下模板是不能复用的.

而且不依赖模板也更容易测试. 

我们看看整个 form 的核心是什么 ? 

就是对一堆有结构的数据, 附上验证逻辑, 然后绑定到各个组件上去与用户互动. 

所以 model drive 的开发流程是 : 定义出有结构的数据 -> 绑定验证 -> 绑定到组件 -> 用户操作 (我们监听并且反应)

这就是有结构的数据 : 

export class AppComponent {
    constructor(private formBuilder: FormBuilder) { console.clear(); }
    registerForm: FormGroup;
    ngOnInit() {
        this.registerForm = this.formBuilder.group({
            firstname: ['', Validators.required],
            address: this.formBuilder.group({
                text : ['']
            }),
            array: this.formBuilder.array([this.formBuilder.group({
                abc : ['']
            })], Validators.required)
        }); 
    }
}

angular 提供了一个 control api, 让我们去创建数据结构, 对象, 数组, 嵌套等等. 

this.formBuilder.group 创建对象

this.formBuilder.array 创建数组 ( angular 还有添加删除数组的 api 哦 )

firlstname : [ '', Validators.required, Validators.requiredAsync ] 这是一个简单的验证绑定, 如果要动态绑定的话也是通过 control api 

control api 还有很多种对数据, 结构, 验证, 监听的操作, 等实际开发之后我才补上吧. 

template drive 其实也是用同一个方式来实现的, 只是 template drive 是通过指令去创建了这些 control, 并且隐藏了起来, 所以其实看穿了也没什么, 我们也可以自己写指令去让 template drive 实现所有的功能.

接下来是绑定到 template 上.

<form [formGroup]="registerForm">
    <input type="text" formControlName="firstname"/> 
    <fieldset formGroupName="address"> 
        <input type="text" formControlName="text"> 
    </fieldset> 
    <div formArrayName="array">
            <div *ngFor="let obj of registerForm.controls.array.controls; let i=index">
            <fieldset [formGroupName]="i"> 
                <input type="text" formControlName="abc"> 
            </fieldset> 
            </div>
    </div>              
</form> 

值得注意的是 array 的绑定, 使用了 i 

特别附上这 2 篇 : 

https://scotch.io/tutorials/how-to-build-nested-model-driven-forms-in-angular-2

https://scotch.io/tutorials/how-to-deal-with-different-form-controls-in-angular-2

 

async validator 

static async(): AsyncValidatorFn { 
    let timer : NodeJS.Timer;  
    return (control: AbstractControl) => { 
        return new Promise((resolve, reject) => {
            clearTimeout(timer);
            timer = setTimeout(() => {
                console.log("value is " + control.value);
                console.log("ajaxing");
                let isOk = control.value == "keatkeat";
         //假设是一个 ajax 啦 setTimeout(()
=> { if (isOk) { return resolve(null); } else { resolve({ sync: true }); } }, 5000); }, 300); }); }; } this.form = this.formBuilder.group({ name: ["abcde", MyValidators.sync(), MyValidators.async()] });

angular 会一直调用 async valid 所以最好是写一个 timer, 不然一直 ajax 很浪费.

 

 

常用的手动调用 : 

this.form.controls["name"].markAsPending(); //async valid 时会是 pending 状态, 然后 setErrors 会自动把 pending set to false 哦
this.form.controls["name"].setErrors({ required : true }); 
this.form.controls["name"].setErrors(null); // null 表示 valid 了   
this.form.controls["name"].markAsTouched();
this.form.controls['name'].updateValueAndValidity(); //trigger 验证 (比如做 confirmPassword match 的时候用到) 
this.form.controls['name'].root.get("age"); //获取 sibling 属性, 验证的时候经常用到, 还支持 path 哦 .get("address.text")
this.form.controls["confirmPassword"].valueChanges.subscribe(v => v); //监听任何变化

这些方法都会按照逻辑去修改更多相关值, 比如 setErrors(null); errors = null 同时修改 valid = true, invalid = false; 

特别说一下 AbstractControl.get('path'),

-当 path 有包含 "." 时 (例如 address.text), ng 会把 address 当成对象然后获取 address 的 text 属性. 但是如果你有一个属性名就叫 'address.text' : "value" 那么算是你自己挖坑跳哦.  

-如果要拿 array 的话是 get('products.0') 而不是 get('products[0]') 哦.

 

更新 : 2016-12-23 

touch and dirty 的区别 

touch 表示被"动"过 ( 比如 input unblur 就算 touch 了 )

dirty 是说值曾经被修改过 ( 改了即使你改回去同一个值也时 dirty 了哦 )

 

更新 : 2017-02-16

概念 : 

当我们自己写 accessor 的时候, 我们也应该 follow angular style 

比如自定义 upload file 的时候, 当 ajax upload 时, 我们应该把 control 的 status set to "PENDING" 通过 control.markAsPending()

pending 的意思是用户正在进行操作, 可能是正在 upload or 正在做验证. 总之必须通过 control 表达出去给 form 知道, form 则可能阻止 submitt 或则是其它特定处理. 

要在 accessor 里调用 formContril 我们需要注入

@Optional() @Host() @SkipSelf() parentControlContainer, 

配合 @Input formControlName 就可以获取到 formControl

最后提一点, 当 control invalid 时, control.value 并不会被更新为 null. 我记得 angular1 会自动更新成 null. 这在 angular2 是不同的。 

 

modelToView( value: any) {
this. formControl. setValue( value, {
emitEvent: false,
emitModelToViewChange: true,
emitViewToModelChange: false
});
}

转载于:https://www.cnblogs.com/keatkeat/p/5821974.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值