@@ -878,6 +878,154 @@ 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
+ * #### 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 ( ) ;
881
1029
}
882
1030
} ;
883
1031
@@ -894,30 +1042,14 @@ function setupModelWatcher(ctrl) {
894
1042
var modelValue = ctrl . $$ngModelGet ( scope ) ;
895
1043
896
1044
// 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
898
1047
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 )
902
1051
) {
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 ) ;
921
1053
}
922
1054
923
1055
return modelValue ;
0 commit comments