@@ -878,6 +878,154 @@ NgModelController.prototype = {
878878 */
879879 $overrideModelOptions : function ( options ) {
880880 this . $options = this . $options . createChild ( options ) ;
881+ } ,
882+
883+ /**
884+ * @ngdoc method
885+ *
886+ * @name ngModel.NgModelController#$processModelValue
887+
888+ * @description
889+ *
890+ * Runs the model -> view pipeline on the current
891+ * {@link ngModel.NgModelController#$modelValue $modelValue}.
892+ *
893+ * The following actions are performed by this method:
894+ *
895+ * - the `$modelValue` is run through the {@link ngModel.NgModelController#$formatters $formatters}
896+ * and the result is set to the {@link ngModel.NgModelController#$viewValue $viewValue}
897+ * - the `ng-empty` or `ng-not-empty` class is set on the element
898+ * - if the `$viewValue` has changed:
899+ * - {@link ngModel.NgModelController#$render $render} is called on the control
900+ * - the {@link ngModel.NgModelController#$validators $validators} are run and
901+ * the validation status is set.
902+ *
903+ * This method is called by ngModel internally when the bound scope value changes.
904+ * Application developers usually do not have to call this function themselves.
905+ *
906+ * This function can be used when the `$viewValue` or the rendered DOM value are not correctly
907+ * formatted and the `$modelValue` must be run through the `$formatters` again.
908+ *
909+ * #### Example
910+ *
911+ * Consider a text input with an autocomplete list (for fruit), where the items are
912+ * objects with a name and an id.
913+ * A user enters `ap` and then selects `Apricot` from the list.
914+ * Based on this, the autocomplete widget will call `$setViewValue({name: 'Apricot', id: 443})`,
915+ * but the rendered value will still be `ap`.
916+ * The widget can then call `ctrl.$processModelValue()` to run the model -> view
917+ * pipeline again, which formats the object to the string `Apricot`,
918+ * then updates the `$viewValue`, and finally renders it in the DOM.
919+ *
920+ * <example module="inputExample" name="ng-model-process">
921+ <file name="index.html">
922+ <div ng-controller="inputController" style="display: flex;">
923+ <div style="margin-right: 30px;">
924+ Search Fruit:
925+ <basic-autocomplete items="items" on-select="selectedFruit = item"></basic-autocomplete>
926+ </div>
927+ <div>
928+ Model:<br>
929+ <pre>{{selectedFruit | json}}</pre>
930+ </div>
931+ </div>
932+ </file>
933+ <file name="app.js">
934+ angular.module('inputExample', [])
935+ .controller('inputController', function($scope) {
936+ $scope.items = [
937+ {name: 'Apricot', id: 443},
938+ {name: 'Clementine', id: 972},
939+ {name: 'Durian', id: 169},
940+ {name: 'Jackfruit', id: 982},
941+ {name: 'Strawberry', id: 863}
942+ ];
943+ })
944+ .component('basicAutocomplete', {
945+ bindings: {
946+ items: '<',
947+ onSelect: '&'
948+ },
949+ templateUrl: 'autocomplete.html',
950+ controller: function($element, $scope) {
951+ var that = this;
952+ var ngModel;
953+
954+ that.$postLink = function() {
955+ ngModel = $element.find('input').controller('ngModel');
956+
957+ ngModel.$formatters.push(function(value) {
958+ return (value && value.name) || value;
959+ });
960+
961+ ngModel.$parsers.push(function(value) {
962+ var match = value;
963+ for (var i = 0; i < that.items.length; i++) {
964+ if (that.items[i].name === value) {
965+ match = that.items[i];
966+ break;
967+ }
968+ }
969+
970+ return match;
971+ });
972+ };
973+
974+ that.selectItem = function(item) {
975+ ngModel.$setViewValue(item);
976+ ngModel.$processModelValue();
977+ that.onSelect({item: item});
978+ };
979+ }
980+ });
981+ </file>
982+ <file name="autocomplete.html">
983+ <div>
984+ <input type="search" ng-model="$ctrl.searchTerm" />
985+ <ul>
986+ <li ng-repeat="item in $ctrl.items | filter:$ctrl.searchTerm">
987+ <button ng-click="$ctrl.selectItem(item)">{{ item.name }}</button>
988+ </li>
989+ </ul>
990+ </div>
991+ </file>
992+ * </example>
993+ *
994+ */
995+ $processModelValue : function ( ) {
996+ var viewValue = this . $$format ( ) ;
997+
998+ if ( this . $viewValue !== viewValue ) {
999+ this . $$updateEmptyClasses ( viewValue ) ;
1000+ this . $viewValue = this . $$lastCommittedViewValue = viewValue ;
1001+ this . $render ( ) ;
1002+ // It is possible that model and view value have been updated during render
1003+ this . $$runValidators ( this . $modelValue , this . $viewValue , noop ) ;
1004+ }
1005+ } ,
1006+
1007+ /**
1008+ * This method is called internally to run the $formatters on the $modelValue
1009+ */
1010+ $$format : function ( ) {
1011+ var formatters = this . $formatters ,
1012+ idx = formatters . length ;
1013+
1014+ var viewValue = this . $modelValue ;
1015+ while ( idx -- ) {
1016+ viewValue = formatters [ idx ] ( viewValue ) ;
1017+ }
1018+
1019+ return viewValue ;
1020+ } ,
1021+
1022+ /**
1023+ * This method is called internally when the bound scope value changes.
1024+ */
1025+ $$setModelValue : function ( modelValue ) {
1026+ this . $modelValue = this . $$rawModelValue = modelValue ;
1027+ this . $$parserValid = undefined ;
1028+ this . $processModelValue ( ) ;
8811029 }
8821030} ;
8831031
@@ -894,30 +1042,14 @@ function setupModelWatcher(ctrl) {
8941042 var modelValue = ctrl . $$ngModelGet ( scope ) ;
8951043
8961044 // if scope model value and ngModel value are out of sync
897- // TODO(perf): why not move this to the action fn?
1045+ // This cannot be moved to the action function, because it would not catch the
1046+ // case where the model is changed in the ngChange function or the model setter
8981047 if ( modelValue !== ctrl . $modelValue &&
899- // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
900- // eslint-disable-next-line no-self-compare
901- ( ctrl . $modelValue === ctrl . $modelValue || modelValue === modelValue )
1048+ // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
1049+ // eslint-disable-next-line no-self-compare
1050+ ( ctrl . $modelValue === ctrl . $modelValue || modelValue === modelValue )
9021051 ) {
903- ctrl . $modelValue = ctrl . $$rawModelValue = modelValue ;
904- ctrl . $$parserValid = undefined ;
905-
906- var formatters = ctrl . $formatters ,
907- idx = formatters . length ;
908-
909- var viewValue = modelValue ;
910- while ( idx -- ) {
911- viewValue = formatters [ idx ] ( viewValue ) ;
912- }
913- if ( ctrl . $viewValue !== viewValue ) {
914- ctrl . $$updateEmptyClasses ( viewValue ) ;
915- ctrl . $viewValue = ctrl . $$lastCommittedViewValue = viewValue ;
916- ctrl . $render ( ) ;
917-
918- // It is possible that model and view value have been updated during render
919- ctrl . $$runValidators ( ctrl . $modelValue , ctrl . $viewValue , noop ) ;
920- }
1052+ ctrl . $$setModelValue ( modelValue ) ;
9211053 }
9221054
9231055 return modelValue ;
0 commit comments