Skip to content

Commit 77e2466

Browse files
committed
feat(ngModel): expose function to run model -> view pipeline
Closes angular#3407 Closes angular#10764
1 parent 20590c0 commit 77e2466

File tree

2 files changed

+80
-24
lines changed

2 files changed

+80
-24
lines changed

src/ng/directive/ngModel.js

+27-24
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,27 @@ NgModelController.prototype = {
878878
*/
879879
$overrideModelOptions: function(options) {
880880
this.$options = this.$options.createChild(options);
881+
},
882+
883+
$setModelValue: function(modelValue) {
884+
this.$modelValue = this.$$rawModelValue = modelValue;
885+
this.$$parserValid = undefined;
886+
887+
var formatters = this.$formatters,
888+
idx = formatters.length;
889+
890+
var viewValue = modelValue;
891+
while (idx--) {
892+
viewValue = formatters[idx](viewValue);
893+
}
894+
if (this.$viewValue !== viewValue) {
895+
this.$$updateEmptyClasses(viewValue);
896+
this.$viewValue = this.$$lastCommittedViewValue = viewValue;
897+
this.$render();
898+
899+
// It is possible that model and view value have been updated during render
900+
this.$$runValidators(this.$modelValue, this.$viewValue, noop);
901+
}
881902
}
882903
};
883904

@@ -894,33 +915,15 @@ function setupModelWatcher(ctrl) {
894915
var modelValue = ctrl.$$ngModelGet(scope);
895916

896917
// if scope model value and ngModel value are out of sync
897-
// TODO(perf): why not move this to the action fn?
918+
// This cannot be moved to the action function, because it would not catch the
919+
// case where the model is changed in the ngChange function or the model setter
898920
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)
921+
// checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
922+
// eslint-disable-next-line no-self-compare
923+
(ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
902924
) {
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-
}
925+
ctrl.$setModelValue(modelValue);
921926
}
922-
923-
return modelValue;
924927
});
925928
}
926929

test/ng/directive/ngModelSpec.js

+53
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,59 @@ describe('ngModel', function() {
603603
expect(ctrl.$modelValue).toBeNaN();
604604

605605
}));
606+
607+
describe('$setModelValue', function() {
608+
it('should run the model -> view pipeline', function() {
609+
var log = [];
610+
611+
ctrl.$formatters.unshift(function(value) {
612+
log.push(value);
613+
return value + 2;
614+
});
615+
616+
ctrl.$formatters.unshift(function(value) {
617+
log.push(value);
618+
return value + '';
619+
});
620+
621+
spyOn(ctrl, '$render');
622+
623+
ctrl.$setModelValue(3);
624+
625+
expect(ctrl.$modelValue).toBe(3);
626+
expect(log).toEqual([3, 5]);
627+
expect(ctrl.$viewValue).toBe('5');
628+
expect(ctrl.$render).toHaveBeenCalledOnce();
629+
});
630+
631+
it('should run the model -> view pipeline even if the value has not changed', function() {
632+
// this is analogue to $setViewValue
633+
spyOn(ctrl, '$render');
634+
635+
ctrl.$setModelValue(3);
636+
637+
expect(ctrl.$modelValue).toBe(3);
638+
expect(ctrl.$render).toHaveBeenCalledOnce();
639+
640+
ctrl.$setModelValue(3);
641+
expect(ctrl.$modelValue).toBe(3);
642+
expect(ctrl.$render).toHaveBeenCalledOnce();
643+
});
644+
645+
it('should not modify the scope value', function() {
646+
// this is analogue to $setViewValue, which does not modify the rendered (DOM) value
647+
scope.$apply('value = 10');
648+
expect(ctrl.$modelValue).toBe(10);
649+
expect(ctrl.$viewValue).toBe(10);
650+
651+
ctrl.$setModelValue(3);
652+
653+
expect(scope.value).toBe(10);
654+
expect(ctrl.$modelValue).toBe(3);
655+
expect(ctrl.$viewValue).toBe(3);
656+
});
657+
658+
});
606659
});
607660

608661

0 commit comments

Comments
 (0)