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