@@ -878,6 +878,158 @@ NgModelController.prototype = {
878
878
*/
879
879
$overrideModelOptions : function ( options ) {
880
880
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
+ * For example, consider a text input with an autocomplete list (for fruit), where the items are
910
+ * objects with a name and an id.
911
+ * A user enters `ap` and then selects `Apricot` from the list.
912
+ * Based on this, the autocomplete widget will call `$setViewValue({name: 'Apricot', id: 443})`,
913
+ * but the rendered value will still be `ap`.
914
+ * The widget can then call `ctrl.$processModelValue()` to run the model -> view
915
+ * pipeline again, which formats the object to the string `Apricot`,
916
+ * then updates the `$viewValue`, and finally renders it in the DOM.
917
+ *
918
+ * @example
919
+ * <example module="inputExample" name="ng-model-process">
920
+ <file name="app.js">
921
+ angular.module('inputExample', [])
922
+ .controller('inputController', function($scope) {
923
+ $scope.items = [
924
+ {name: 'Apricot', id: 443},
925
+ {name: 'Clementine', id: 972},
926
+ {name: 'Durian', id: 169},
927
+ {name: 'Jackfruit', id: 982},
928
+ {name: 'Strawberry', id: 863}
929
+ ];
930
+ })
931
+ .component('basicAutocomplete', {
932
+ bindings: {
933
+ items: '<',
934
+ onSelect: '&'
935
+ },
936
+ templateUrl: 'autocomplete.html',
937
+ controller: function($element, $scope) {
938
+ var that = this;
939
+ var ngModel;
940
+
941
+ that.$postLink = function() {
942
+ ngModel = $element.find('input').controller('ngModel');
943
+
944
+ ngModel.$formatters.push(function(value) {
945
+ return (value && value.name) || value;
946
+ });
947
+
948
+ ngModel.$parsers.push(function(value) {
949
+ var match = value;
950
+ for (var i = 0; i < that.items.length; i++) {
951
+ if (that.items[i].name === value) {
952
+ match = that.items[i];
953
+ break;
954
+ }
955
+ }
956
+
957
+ return match;
958
+ });
959
+ };
960
+
961
+ that.setOnMatch = function() {
962
+ if (angular.isObject(that.searchTerm)) {
963
+ that.onSelect({item: that.searchTerm});
964
+ }
965
+ };
966
+
967
+ that.selectItem = function(item) {
968
+ ngModel.$setViewValue(item);
969
+ ngModel.$processModelValue();
970
+ };
971
+ }
972
+ });
973
+ </file>
974
+ <file name="index.html">
975
+ <div ng-controller="inputController" style="display: flex;">
976
+ <div style="margin-right: 30px;">
977
+ Search Fruit:
978
+ <basic-autocomplete items="items" on-select="selectedFruit = item"></basic-autocomplete>
979
+ </div>
980
+ <div>
981
+ Model:<br>
982
+ <pre>{{selectedFruit | json}}</pre>
983
+ </div>
984
+ </div>
985
+ </file>
986
+ <file name="autocomplete.html">
987
+ <div>
988
+ <input type="search" ng-model="$ctrl.searchTerm" ng-change="$ctrl.setOnMatch()" />
989
+ <ul>
990
+ <li ng-repeat="item in $ctrl.items | filter:$ctrl.searchTerm">
991
+ <button ng-click="$ctrl.selectItem(item)">{{ item.name }}</button>
992
+ </li>
993
+ </ul>
994
+ </div>
995
+ </file>
996
+ * </example>
997
+ *
998
+ */
999
+ $processModelValue : function ( ) {
1000
+ var viewValue = this . $$format ( ) ;
1001
+
1002
+ if ( this . $viewValue !== viewValue ) {
1003
+ this . $$updateEmptyClasses ( viewValue ) ;
1004
+ this . $viewValue = this . $$lastCommittedViewValue = viewValue ;
1005
+ this . $render ( ) ;
1006
+ // It is possible that model and view value have been updated during render
1007
+ this . $$runValidators ( this . $modelValue , this . $viewValue , noop ) ;
1008
+ }
1009
+ } ,
1010
+
1011
+ /**
1012
+ * This method is called internally to run the $formatters on the $modelValue
1013
+ */
1014
+ $$format : function ( ) {
1015
+ var formatters = this . $formatters ,
1016
+ idx = formatters . length ;
1017
+
1018
+ var viewValue = this . $modelValue ;
1019
+ while ( idx -- ) {
1020
+ viewValue = formatters [ idx ] ( viewValue ) ;
1021
+ }
1022
+
1023
+ return viewValue ;
1024
+ } ,
1025
+
1026
+ /**
1027
+ * This method is called internally when the bound scope value changes.
1028
+ */
1029
+ $$setModelValue : function ( modelValue ) {
1030
+ this . $modelValue = this . $$rawModelValue = modelValue ;
1031
+ this . $$parserValid = undefined ;
1032
+ this . $processModelValue ( ) ;
881
1033
}
882
1034
} ;
883
1035
@@ -894,30 +1046,14 @@ function setupModelWatcher(ctrl) {
894
1046
var modelValue = ctrl . $$ngModelGet ( scope ) ;
895
1047
896
1048
// if scope model value and ngModel value are out of sync
897
- // TODO(perf): why not move this to the action fn?
1049
+ // This cannot be moved to the action function, because it would not catch the
1050
+ // case where the model is changed in the ngChange function or the model setter
898
1051
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 )
1052
+ // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
1053
+ // eslint-disable-next-line no-self-compare
1054
+ ( ctrl . $modelValue === ctrl . $modelValue || modelValue === modelValue )
902
1055
) {
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
- }
1056
+ ctrl . $$setModelValue ( modelValue ) ;
921
1057
}
922
1058
923
1059
return modelValue ;
0 commit comments