05月23, 2017

【译】Angular 2 : 如何在自定义控件中使用Control Value Accessor

原文链接:Angular 2: Connect your custom control to ngModel with Control Value Accessor.

注意:angular的版本更新频繁,本文的示例也许不能跑在最新版本的angular上,但是里面的核心概念依然有效。

当你学习了angular2,就可以动手构建自定义表单,然后你把里面的各种控件剥离出来,准备作为独立的组件弄到你的应用里面去。

于是你使用ngModel和name将自定义组件插入HTML表单,然后刷新浏览器,打算看看效果,结果发现在浏览器控制台上尽是各种乱七八糟的错误。

事实证明,之前的学习还远远不够,我们今天就是要帮助你构建能使用ngModel特性的自定义组件。

接下来让代码说话。

注意:从Angular 2.0RC5开始,如果要使用ngModel特征,你需要导入模板驱动表单或响应式表单模块。在本文后面我们会告诉你怎么做。

示例中我们将使用最基本的input组件。你也许会说,这太简单了,完全可以用HTML原生input元素来代替,没错,是这样的。

但是我们的目的是尽量展示怎么样将组件暴露給接口,如果示例太过复杂,我们就很难将重点放在要讲述的要点上。一旦你明白是怎么回事,你就可以基于我们的示例修改并完成一个复杂的控件。注意,如果要完成一个能够正常运行的控件,在背后有大量隐形的工作要做。

首先,我们看一下组件的示例代码:

import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

const noop = () => {
};

export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true
};

@Component({
    selector: 'custom-input',
    template: `<div class="form-group">
                    <label><ng-content></ng-content>
                        <input [(ngModel)]="value"
                                class="form-control"
                                (blur)="onBlur()" >
                    </label>
                </div>`,
    providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class CustomInputComponent implements ControlValueAccessor {

    //The internal data model
    private innerValue: any = '';

    //Placeholders for the callbacks which are later providesd
    //by the Control Value Accessor
    private onTouchedCallback: () => void = noop;
    private onChangeCallback: (_: any) => void = noop;

    //get accessor
    get value(): any {
        return this.innerValue;
    };

    //set accessor including call the onchange callback
    set value(v: any) {
        if (v !== this.innerValue) {
            this.innerValue = v;
            this.onChangeCallback(v);
        }
    }

    //Set touched on blur
    onBlur() {
        this.onTouchedCallback();
    }

    //From ControlValueAccessor interface
    writeValue(value: any) {
        if (value !== this.innerValue) {
            this.innerValue = value;
        }
    }

    //From ControlValueAccessor interface
    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    //From ControlValueAccessor interface
    registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }

}

接下来看看怎么使用这个自定义控件:

<form>

    <custom-input name="someValue"
                  [(ngModel)]="dataModel">
          Enter data:
    </custom-input>

  </form>

预览效果如下:

请移步原文链接:http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel

我到底看到了什么?亮瞎我的钛合金狗眼

不要慌,接下来我们会解释这是怎么回事,不过在此之前我们应该先看看两个原则:

  • 尽量直接使用HTML原生的表单元素,而不是自己实现,优先考虑原生表单元素。
  • 当你需要功能更强的或者需要创建自定义表单控件的时候,请使用Angular 2提供的基础设施。

现在开始撸一遍代码

第一步:把代码跑起来

因为要用ngModel和Angular 2提供的其他表单功能,我们需要导入两个表单模块(module)。 在本文我们使用模板驱动表单(Template Driven Forms)。

首先,我们先创建应用模块(Application Module)。

//Creating our Application Module
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { CustomInputComponent } from './custom-input.component';

@NgModule({
    imports: [BrowserModule, FormsModule],
    declarations: [AppComponent, CustomInputComponent],
    bootstrap: [AppComponent]
})
export class AppModule {}

注意只要是跟浏览器有关的应用,都需要导入BrowserModule。由于我们还要使用ngModel这样的表单特征,因此还需要导入FormsModule

代码中我们声明了AppComponent,并设置为Bootstrap Component。同时我们还声明了CustomInputComponent,这样在应用中的其他地方也可以使用该组件。

配置好Application Module后,我们就可以定义应用的启动(bootstrap)代码。

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

如果这段代码看起来有困难,那说明你需要补一下官方的Angular文档,拿去不谢

第二步:组装NG_VALUE_ACCESSORmulti-provider

在Angular2中可以注册多个provider,也可以扩展已有的provider(通过扩展的方式避免重复的provider冲突)。我们需要把我们自定义的信息告知NG_VALUE_ACCESSOR令牌(Angular2 Token):我们的代码要进行数据绑定。

export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true
};

上面的代码通知了系统我们的组件是什么。其中最关键的是我们使用了forwardRef,这个的作用是在注册provider的时候,我们的组件类还没定义(defined)好,我们需要通知Provider构造器,请等待组件类完成定义。

译者注:这段比较难理解,大概意思是在Provider和Component互相依赖的情况下,其中一方需要等待另外一方完成后再开始自己这方的初始化。

定义好了Provider后,接下来就是声明Provider,这样组件才能使用Provider。

    providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]

现在Angular 2知道我们的表单控件类了,不过事情还没完。

第三步:实现ControlValueAccessor - 关键点

在自定义组件里面我们需要管理数据和事件,因为我们需要实现Angular 2提供的ControlValueAccessor接口。

代码示例如下(实现接口,完成和数据模型的协作):

//The internal data model
    private innerValue: any = '';

    //Placeholders for the callbacks which are later provided
    //by the Control Value Accessor
    private onTouchedCallback: () => void = noop;
    private onChangeCallback: (_: any) => void = noop;

    //get accessor
    get value(): any {
        return this.innerValue;
    };

    //set accessor including call the onchange callback
    set value(v: any) {
        if (v !== this.innerValue) {
            this.innerValue = v;
            this.onChangeCallback(v);
        }
    }

    //Set touched on blur
    onBlur() {
        this.onTouchedCallback();
    }

    //From ControlValueAccessor interface
    writeValue(value: any) {
        if (value !== this.innerValue) {
            this.innerValue = value;
        }
    }

    //From ControlValueAccessor interface
    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    //From ControlValueAccessor interface
    registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }

首先我们先看一下最后三个方法。ControlValueAccessor接口定义的这三个方法是我们的组件内部和组件外部进行交流的口子,我们需要在代码中实现这种交互。

writeValue方法将来自外部的数据写入到内部的数据模型,例如:你用ngModel将控件和数据绑定。

registerOnChange方法接受一个回调方法,在数据改变的时候,你可以通过调用这个回调方法通知外部数据变了。同时你可以将修改后的数据(译者注:可以但是不是必须)作为参数传递出去。

registerOnTouched方法接受一个回调方法,当你想把控件的状态改变为touched的时候可以调用这个方法。通过Angular 2給DOM元素标签添加正确的touched状态和类。

注意:Angular2会提供并自动注册这些回调功能,但是在此之前我们需要提供一个空实现(保证代码可运行并且不出错)。

看完这三个函数,接下来轮到组件内部代码了。

  • 回调函数占位(空的实现,在运行时被Angular2替代)
  • 内部数据的getter和setter
  • 捕获内部控件的blur事件的函数,并将整个组件的状态设置为touched

这里面并没有什么高科技,用心都能学会。

至此,我们的新的自定义控件支持ngControl和ngModel指令了。

骚年,动起来吧。

如果你还想继续提升,掌握Angular2更牛的表单控件,那就去研究一下Angular Meterial 2的源码,和本文主题有关的问题都能在里面找到答案。

本文链接:http://www.xiaojichao.com/post/linkup-custom-control-to-ngcontrol-ngmodel.html

-- EOF --

Comments