AngularJS----ngModelController

时间:2022-07-17 13:52:05

文档地址:https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

首先声明:DOM value 与view value是完全不同的概念,不能保证两者的值是相等的。

简介

ngModelController是为ng-model指令提供的API。

这个控制器包含了为数据绑定(data-binding)、校验(validation)、CSS更新(CSS updates)、值的格式化和解析(value formatting and parsing)等功能。它不包含任何处理DOM渲染或者DOM事件监听的逻辑。这些与DOM相关的逻辑应该由那些使用ngModelController进行数据绑定来控制元素的指令提供。

自定义控件的例子

这个例子演示了一个自定义控件怎么使用ngModelController实现数据绑定。

注意不同指令(contenteditable、ng-model、required)之间是怎么共同合作实现了预想中的结果。

contenteditable是一个HTML5属性,告诉浏览器,元素的内容可以被用户编辑。

我们使用$sce服务和$sanitize服务来自动移除”bad”内容,比如行内事件监听器(e.g. ‘<span onclick=”…”>’)。

However, as we are using $sce the model can still decide to provide unsafe content if it marks that content using the $sce service.

CSS:

[contenteditable] {
border: 1px solid black;
background-color: white;
min-height: 20px;
} .ng-invalid {
border: 1px solid red;
}

关于ngSanitize:angular-ngSanitize模块-$sanitize服务详解

JS:

angular.module('customControl', ['ngSanitize']).directive('contenteditable', ['$sce', function($sce) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model // Specify how UI should be updated
ngModel.$render = function() {
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
}; // Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read(); // initialize // Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if (attrs.stripBr && html === '<br>') {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);

HTML:

<form name="myForm">
<div contenteditable name="myWidget" ng-model="userContent" strip-br="true" required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent" aria-label="Dynamic textarea"></textarea>
</form>

ngModelcontroller的属性说明

$viewValue

控件视图中的实际值。对于input元素,这个值是字符串。(参考$setViewValue了解什么时候设置$viewValue的值)

$modelValue

控件所绑定的模型中的值。

$parsers

一个函数组成的数组。作为一个管道(pipeline),每当控件用一个来自DOM的新的$viewValue(通常是通过用户输入)更新ngModelcontroller时执行$parsers中的函数。参考$setViewValue了解详细的生命周期说明。

注意,当绑定到ngModel的表达式是通过编程的方式改变时,$parses中的函数不会被调用。

这些函数按照在数组中的顺序被调用,前一个函数的返回值传递给下一个。最后一个函数的返回值被转发给$validators集合(验证器集合)。

解析器($parsers)用于消除或者转换$viewValue的值。

如果从一个解析器($parsers中的某一个函数)返回了undefined,那就意味着出现了一个解析错误。这种情况下,没有$validators会运行。而且,除非ngModelOptions.allowInvalid被设置为true,否则ngModel将会被设置为undefined。这个解析错误被存储在ngModel.$error.parse中。

下面是一个解析器将输入值(input value,或者说view value)转换为小写的例子:

function parse(value) {
if(value) {
return value.toLowerCase();
}
}
ngModelController.$parsers.push(parse);

$formatters

一个函数组成的数组。作为一个管道,每当绑定到ngModel上的表达式的值通过编程的方式改变时,数组中的函数将会被执行。当控件中的值是通过用户交互改变的时候,$formatters中的函数将不会被执行。

$formatters用于在控件(模型)中格式化或者转换$modelValue。

$formatters中的函数按照在数组中的倒序被调用,每一个函数执行后的返回值会传递给下一个函数。最后一个函数的返回值被作为最终的DOM值(DOM value)使用。

下面是一个formatter(不知道怎么翻译)将model value转换为大写的例子:

function format(value) {
if(value) {
return value.toUpperCase();
}
}
ngModelController.$formatters.push(format);

$validators

一个验证器(validators)的集合(相当于一个字面量对象),当model value需要被更新时会被应用。这个对象的键值(key value)是验证器(validator,是一个函数,可以进行相应的验证运算)的名字。model value传入验证器,作为验证器的一个参数。验证器根据验证结果返回一个Boolean值。

例子

NgModelcontroller.$fvalidators.validCharacters = function(modelValue,viewValue) {
var value = modelValue || viewValue;
return /[0-9]+/.test(value) &&
/[a-z]+/.test(value) &&
/[A-Z]+/.test(value) &&
/\W+/.test(value);
};

$asyncValidators

异步验证。一个校验的集合,用来进行异步验证(e.g. 一个HTTP请求 )。

当校验函数在模型校验期间(model validation process)运行时,校验函数会返回一个promise。一旦这个promise被delivered,当满足校验时,那么校验的状态(validation status)将会被设为true;当不满足时,被设为false。

当异步校验器(asynchronous validators)被触发时,每一个校验器都会并行运行,并且只有当所有校验器都满足后,model value才会被更新。只要任一校验器没有被满足,那么对应的键(key,可以理解为这个校验器的名字)将会被添加到ngModelcontroller的$pending属性中。

当然,只有当所有的同步校验器(synchronous validators)都校验通过之后,所有的异步校验器才会运行。

注意:如果使用了$http服务,那么为了校验通过,服务器返回一个成功的HTTP响应状态码是非常重要的。为了使其不满足校验,那么就返回‘4xx’状态码。

例子

ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue; // Lookup user by username
return $http.get('/api/users/' + value).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
});
};

$viewChangeListeners

一个函数组成的数组。当view value发生变化时,其中的函数将被执行。函数被调用的时候没有参数,而且函数的返回值也会被忽略。这可以被用于代替额外的用于监听model value的$watches。

$error

An object hash with all failing validator ids as keys.

(如果$validators中的某一个验证函数返回了false,那么这个验证函数的名字就成为$error的一个键,这个键被赋值为true)。

其他的一些属性:

$pending – object  -- An object hash with all pending(待定) validator ids as keys(validator ids是验证函数的名字).

$untouched -- boolean -- True if control has not lost focus yet.

$touched -- boolean -- True if control has lost focus.

$pristine -- boolean -- True if user has not interacted with the control yet.

$dirty -- boolean -- True if user has already interacted with the control.

$valid -- boolean -- True if there is no error.

$invalid -- boolean -- True if at least one error on the control.

$name -- string -- The name attribute of the control.

ngModelcontroller的方法说明

$render();

当视图(view)需要被更新时调用。使用ng-model的那个指令负责实现这个方法。

注意:$render函数的内部逻辑是需要开发者根据自己的需求自己实现的,其本质上的作用是将DOM value 同步为 view value -- $viewValue。

在下列情形时,$render()方法才会被调用:

  • $rollbackViewValue()被调用的时候。如果我们把view value回滚到最后提交的那个值,那么$render()被调用来更新这个输入控件(input control)。
  • 通过编程方式改变ng-model指令所引用的那个值并且$modelValue和$viewValue都与上次的值不同时。

因为ng-model不进行深检测(watch),$render()只在$modelValue和$viewValue的值确实与之前的值不同时才调用。如果$modelValue或者$viewValue的值为对象(object)而不是字符串(string)或者数字(number),并且你只更改了这个对象的一个属性的值时,那么$render()将不会被调用。

$isEmpty(value);

当我们要判断一个输入值是否为空时,可以调用这个函数。

比如,有些指令根据输入的值是否为空而决定是否执行。

默认情况下,$isEmpty函数检测输入值是否是‘undefined’、‘’’’、‘null’或者‘NaN’。

对于输入指令,空值的概念可能与默认定义的那些不同,此时你可以重写这个方法。指令‘checkboxInputType’就是这么做的,因为对于这个指令来说‘false’意味着空值。

这个函数的参数是要检测的那个输入的值。

该函数返回一个boolean值,true意味着检测的那个值是空。

$setPristine();

把控件重置到它最初的状态(pristine state)。

调用这个方法用于移除控件class中的‘ng-dirty’并且把这个控件重置到最初的状态(添加‘ng-pristine’class)。当一个控件自从被第一次编译之后没有再被更改,那么可以认为这个模块(原文中用的是model)是最初的状态。

$setDirty();

将控件设置为脏状态(dirty state)。

调用这个方法用于移除控件的‘ng-pristine’class,并把这个控件设置为脏状态(添加‘ng-dity’class)。如果一个控件自从被第一次编译之后被更改,那么可以认为这个模型(原文中用的是model)处于脏状态。

$setUntouched();

把控件设置为untouched state。

调用这个方法用于移除控件的‘ng-touched’class,并且把这个控件设置为untouched(添加‘ng-untouched’class)。在编译时,一个模型默认被设为untouched。然而,如果这个模型已经被用户touched,这个函数可以被用来恢复模型的状态。

$setTouched();

把控件设置为touched state。

调用这个方法用于移除‘ng-untouched’class,并且把这个控件设置为touched state(添加‘ng-touched’class)。如果用户第一次将焦点定位到了控件元素上,然后又将焦点从控件上移开,那么就认为这个模型已经被touched。

$rollbackViewValue();

取消更新(update)并且重置(reset)输入元素的值,阻止对$modelValue更新。

这个方法会把数据模型的值返回给视图,同时取消所有的将要发生的延迟更新事件。

如果有一个输入控件使用了ng-model-options,并且给ng-model-options设置了去抖动更新(debounced updates)或者设置了基于特殊事件的更新(比如blur事件),那么就存在一段时期,$viewValue与$modelValue不是同步的。

在这种情况下,你可以用$rollbackViewValue()手动取消这种去抖动更新或者说基于事件的更新(debounced / future update),并且重置这个输入(控件的)值为最后提交的那个视图值(view value)(实际上是将视图值与当前的$modelView同步)。

如果你试图在这些去抖动更新函数执行之前或者基于事件更新的事件发生之前(before these debounced/future events have resolved/occurred)通过编程的方式更新ngModel指令的$modelValue的值,那么你可能遇到困难。因为AngularJS的脏检测机制不能分清数据模型到底有没有确实发生改变。

$rollbackViewValue方法应该在通过编程方式改变一个输入控件的数据模型之前调用,这个控件可能有被挂起的事件(debounced/future events)。这对于确保输入控件的值能顺利地与新的数据模型同步并且同时取消那些挂起的事件是很重要的。

举例子之前脑海中要先有这么一个想法:ng-model-options的作用是延迟view value立即同步给model value(可以通过事件、时间或者自定义的去抖动函数实现);$rollbackViewValue的作用是将当前的model value立即同步给view value,并且取消挂起的延迟同步函数或事件等。而且一般情况下是用不到$rollbackViewValue方法的,这个方法一般在使用了ng-model-options的情形下才可能用得到。

例子

这里要说一句,官方列出的例子容易让人糊涂,所以这里对官方的例子做了一些修改,并且参考了网上其他博主的文章。

我们都知道,一般情况下使用ng-model,view value与model value是实时同步更新的。下面这个例子是使用了ng-model-options的情形:

js代码:

angular.module('cancel-update-example', []).controller('CancelUpdateController', ['$scope', function($scope) {
$scope.model = {value1: '', value2: ''};
$scope.setValue = function(e,value) {
$scope.model[value] = 'test';
console.log(value+'****************'+$scope.model[value]);
};
$scope.setEmpty = function(e, value, rollback) {
if (e.keyCode === 27) {
console.log(27);
console.log('rollback*********'+rollback);
e.preventDefault();
if (rollback) {
console.log('rollback*********'+rollback);
$scope.myForm[value].$rollbackViewValue();
}
//$scope.model[value] = '';
}
console.log(value+'****************'+$scope.model[value]);
}; }]);

HTML代码:

<div ng-controller="CancelUpdateController">
<form name="myForm" ng-model-options="{ updateOn: 'blur' }">
<div>
<p id="inputDescription1">Without $rollbackViewValue():</p>
<input name="value1" aria-describedby="inputDescription1" ng-model="model.value1"
ng-keydown="setEmpty($event, 'value1')">
value1: "{{ model.value1 }}"
</div> <div>
<p id="inputDescription2">With $rollbackViewValue():</p>
<input name="value2" aria-describedby="inputDescription2" ng-model="model.value2"
ng-keydown="setEmpty($event, 'value2', true)">
value2: "{{ model.value2 }}"
</div>
</form> <button ng-click="setValue($event, 'value1')" type="button">reset value1</button>
<button ng-click="setValue($event, 'value2')" type="button">reset value2</button>
</div>

ng-model-options=”{uodateOn:’blur’}”的意思是在控件失去焦点的时候将view value同步到model value。

上面例子中,当焦点没有从输入控件上移开时,不管怎么输入,我们输入的值都不会同步到model value。只有当输入控件失去焦点时,我们输入的值才会同步到model value。

对于value1我们先在输入框随便输入一些内容,可以发现输入框后的value1并没有随着我们的输入而立即同步出来,因为我们用ng-model-options设置了失去焦点后才更新。此时,我们点击按钮通过编程的方式把value1 的model value的值设置为test,发现model value的更改立即就同步到了view value。以上情形说明ng-model-options只对view value同步到model value起延迟作用,对model value同步到view value不起作用。

对于value2对应的输入框,给他添加键盘按下事件,就是如果按下Esc键,就调用$rollbackViewValue方法。我们先点击按钮,通过编程把value2对应的model value更改为test,此时输入框中的内容也被同步为了test,然后再在value2对应的输入框中输入内容,然后再按下Esc键,我们会发现输入框里的内容又变回了test。这是因为虽然我们在输入框里输入了内容,但是这个内容并没有同步到model value,此时的model value还是原来的test,此时调用$rollbackViewValue,当前的model value立即同步到了view value,所以输入框中的内容也就变成了test。

下面是$rollbackViewValue在AngularJS中的源码:

$rollbackViewValue: function() {
this.$$timeout.cancel(this.$$pendingDebounce);
this.$viewValue = this.$$lastCommittedViewValue;
this.$render();
},

$validate方法

运行每一个注册过的验证器(validators),首先是同步验证器,然后是异步验证器。

如果验证变更无效,除非ngModelOptions的ngModelOptions.allowInvalid的值为true,否则数据模型的值(model value)将会被设置为‘undefined’。

如果验证变更有效,数据模型的值(model value)将会被设置为最后的那个可用的有效的$modelValue的值,也就是说要么是最后解析的那个值,要么是来自作用域的最后设置的那个值。

$commitViewValue方法

提交挂起的更新到$modelValue。Commit a pending update to the $modelValue.

值的更新同步可能因为去抖动事件(debounced event)或者通过ng-model-options设置的future event而挂起。这个方法很少使用,因为ngModelController通常会处理对输入时间的响应。

$setViewValue方法

这个方法用于更新view value。($setViewValue方法用来将view value -- $viewValue 同步为DOM value)

注意DOM value与view value不是一回事。

当一个控件想要更改view value时,可以调用这个方法。特别地,这是在DOM事件处理程序中完成的。比如,对于input控件,当输入值变更时调用这个方法;对于select控件,当某一个option被选中时调用这个方法。

当$setViewValue被调用时,这个新值将会被提交给$parsers和$validators管道。如果没有特殊的ngModelOptions配置项,那么这个值会被直接传递给$parsers管道。这个过程之后,$validators和$asyncValidators会被调用,然后这个值会被同步给$modelValue。

最后这个值会赋给ng-model属性指定的表达式,并且调用所有注册的变更监听器(change listeners,保存在$viewChangeListeners数组中)。

如果ng-model-options指令指定了updateOn,但是没有使用default触发器,那么所有的动作将会挂起直到updateOn指定的事件在DOM元素上触发为止。

如果ng-model-options指令为特殊事件使用了一个自定义的防抖函数,那么所有这些动作也会被防抖动处理。

要注意的是,$digest只会在updateOn指定的事件触发时或者如果debounce字段指定时间结束之后被触发。

当使用标准的输入控件时,view value通常是一个字符串(在某些情况下会被解析为其他的类型,比如用于input[date]的Date对象)。然而,自定义控件可能也会把对象传递给这个方法,在这种情况下,我们应该在把这个对象传递给$setViewValue之前将这个对象做一个拷贝。这是因为,ngModel不会对对象执行深检测,他只会检查标识符是否发生了改变。如果你仅仅改变了这个对象上的某一个属性的值,那么ngModel将会认为这个对象没有被改变,从而不会调用$parsers和$validators管道。基于这个原因,一旦对象被传给了$setViewValue,你不应该改变其拷贝的属性。否则,可能导致作用域上的model value发生错误的改变。

注意:无论如何(任何情况下),传入$setViewValue方法的值应该总是体现控件当前的值。比如,如果你对一个输入控件调用$setViewValue方法,你应该把输入控件的DOM value传给这个方法。否则的话,这个控件和作用域的数据模型(scope model)将不会同步。同时还要注意,$setViewValue方法不会调用$render或者说不会以任何方式更改控件的DOM value。如果我们想通过编程的方式更改控件的DOM value,我们应该通过ngModel指令绑定的作用域的表达式进行更改。这个新值将会被model controller(我觉得这个控制器应该指的是ngModelController)获取,这个控制器会将这个值通过$formatters格式化,通过$render将这个值更新到DOM,最后调用$validate验证这个值。

$setViewValue方法可以接收两个参数,第一个参数是来自视图的值(我觉得,准确地说应该是DOM value)。第二个参数是一个字符串,是一个事件的名字,通过这个事件触发更新(设置view value的值),这个参数不是必须的。

$overrideModelOptions方法

作用是通过编程的方式重写model options的配置。

调用这个方法时,先前的ModelOptions的值不会被修改。取而代之的是创建一个新的ModelOptions对象,这个新创建的对象会重写或继承先前的那个ModelOptions对象的值。(同名就重写)

查看ngModelOptions指令的相关内容了解有哪些可配置项以及model option的继承机制是如何工作的。

注意这个方法不能重写getterSetter属性的值。

注意这个函数只影响在ngModelController上设置的选项,而不是在ngModelOptions指令上的选项,而这些选项可能是最初获得的。

这个方法的参数是一个对象字面量。

$processModelValue();

这个函数基于当前$modeValue在model value --> view value管道中运行。

该方法在以下情形时执行:

1、$modelValue通过$formatters管道并且将得到的结果设置给$viewValue时;

2、给元素设置ng-empty或ng-not-empty类名时;

3、如果$viewValue被改变时:

$render函数被调用

验证状态被设置和$validators被运行

当绑定在作用于上的值改变时,这个方法被ngModel在内部调用。开发者不需要自己调用这个方法。

当$viewValue或者渲染的DOM value没有被正确的格式化,$modelValue必须再次通过$formatters管道时,可以使用这个函数。

$setValidity方法

这个方法的作用是更改验证状态,并通知表单。

这个方法可以在$parsers / $formatters中或者自定义的验证工具中调用。

然而,在绝大多数情况下使用ngModel.$validators和ngModel.$asyncValidators就已经足够了。ngModel.$validators和ngModel.$asyncValidators会自动调用$setValidity。

这个方法可以接收两个参数:

第一个参数是一个字符串,是报错的验证器的名字(validationErrorKey Name of the validator)。validationErrorKey将会被赋值给$error[validationErrorKey]或者$pending[validationErrorKey](对于未通过的$asyncValidators),so that it is available for data-binding。

validationErrorKey应该用驼峰命名法,并且会被转化为用破折号链接的类名。比如,myError 将会被转换为ng-valid-my-error和ng-invalid-my-error类名,绑定的时候可以这样绑定{{someForm.someControl.$error.myError}}。

第二个参数是一个布尔值。表示一个验证状态,可以传入以下状态:valid(true)、invalid(false)、pending(undefined)、skipped(null)。

Pending用于未通过验证的$asyncValidators。Skipped被AngularJS用于当解析错误而导致验证器(validators)不运行时或者由于$validators中任意一个验证函数未通过而导致$asyncValidators不起作用时。