Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit c1a42c9

Browse files
committed
feat(ngModelOptions): support submit trigger
1 parent 613a5cc commit c1a42c9

File tree

3 files changed

+145
-31
lines changed

3 files changed

+145
-31
lines changed

src/ng/directive/form.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ function FormController(element, attrs, $scope, $animate) {
286286
* hitting enter in any of the input fields will trigger the click handler on the *first* button or
287287
* input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
288288
*
289+
* Note that any pending `ngModelOptions` changes will take place immediately when an enclosing form
290+
* is submitted thanks to `ngForm` broadcasting a scope event with name `$updateInputModels`.
291+
* Use `ngSubmit` to have access to the updated model since `ngClick` events will occur before the
292+
* model is updated.
293+
*
289294
* @param {string=} name Name of the form. If specified, the form controller will be published into
290295
* related scope, under this name.
291296
*
@@ -381,19 +386,23 @@ var formDirectiveFactory = function(isNgForm) {
381386
// IE 9 is not affected because it doesn't fire a submit event and try to do a full
382387
// page reload if the form was destroyed by submission of the form via a click handler
383388
// on a button in the form. Looks like an IE9 specific bug.
384-
var preventDefaultListener = function(event) {
389+
var handleFormSubmission = function(event) {
390+
scope.$apply(function() {
391+
scope.$broadcast('$updateInputModels', event);
392+
});
393+
385394
event.preventDefault
386395
? event.preventDefault()
387396
: event.returnValue = false; // IE
388397
};
389398

390-
addEventListenerFn(formElement[0], 'submit', preventDefaultListener);
399+
addEventListenerFn(formElement[0], 'submit', handleFormSubmission);
391400

392401
// unregister the preventDefault listener so that we don't not leak memory but in a
393402
// way that will achieve the prevention of the default action.
394403
formElement.on('$destroy', function() {
395404
$timeout(function() {
396-
removeEventListenerFn(formElement[0], 'submit', preventDefaultListener);
405+
removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
397406
}, 0, false);
398407
});
399408
}

src/ng/directive/input.js

+44-28
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;
1616
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
1717
var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
1818
var TIME_REGEXP = /^(\d\d):(\d\d)$/;
19-
var DEFAULT_REGEXP = /(\b|^)default(\b|$)/;
19+
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
2020

2121
var inputType = {
2222

@@ -878,6 +878,25 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
878878
}
879879
}
880880

881+
function addUpdateOnListeners(scope, element, options, listener) {
882+
if (options) {
883+
if (options.updateOn) {
884+
element.on(options.updateOn, function(ev) {
885+
scope.$apply(function() {
886+
listener(ev);
887+
});
888+
});
889+
}
890+
891+
scope.$on('$updateInputModels', function(scopeEvent, ev) {
892+
// Since this event can be triggered manually, we pass a dummy submit event
893+
// in case no 'ev' argument is passed. This is important since $setViewValue
894+
// will never debounce stuff that come from 'submit' trigger.
895+
listener(ev || {type: 'submit'});
896+
});
897+
}
898+
}
899+
881900
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
882901
var validity = element.prop('validity');
883902

@@ -924,11 +943,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
924943
}
925944
};
926945

927-
// Allow adding/overriding bound events
928-
if (ctrl.$options && ctrl.$options.updateOn) {
929-
// bind to user-defined events
930-
element.on(ctrl.$options.updateOn, listener);
931-
}
946+
addUpdateOnListeners(scope, element, ctrl.$options, listener);
932947

933948
// setup default events if requested
934949
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
@@ -1205,20 +1220,18 @@ function radioInputType(scope, element, attr, ctrl) {
12051220

12061221
var listener = function(ev) {
12071222
if (element[0].checked) {
1208-
scope.$apply(function() {
1209-
ctrl.$setViewValue(attr.value, ev && ev.type);
1210-
});
1223+
ctrl.$setViewValue(attr.value, ev && ev.type);
12111224
}
12121225
};
12131226

1214-
// Allow adding/overriding bound events
1215-
if (ctrl.$options && ctrl.$options.updateOn) {
1216-
// bind to user-defined events
1217-
element.on(ctrl.$options.updateOn, listener);
1218-
}
1227+
addUpdateOnListeners(scope, element, ctrl.$options, listener);
12191228

12201229
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
1221-
element.on('click', listener);
1230+
element.on('click', function(ev) {
1231+
scope.$apply(function() {
1232+
listener(ev);
1233+
});
1234+
});
12221235
}
12231236

12241237
ctrl.$render = function() {
@@ -1237,19 +1250,17 @@ function checkboxInputType(scope, element, attr, ctrl) {
12371250
if (!isString(falseValue)) falseValue = false;
12381251

12391252
var listener = function(ev) {
1240-
scope.$apply(function() {
1241-
ctrl.$setViewValue(element[0].checked, ev && ev.type);
1242-
});
1253+
ctrl.$setViewValue(element[0].checked, ev && ev.type);
12431254
};
12441255

1245-
// Allow adding/overriding bound events
1246-
if (ctrl.$options && ctrl.$options.updateOn) {
1247-
// bind to user-defined events
1248-
element.on(ctrl.$options.updateOn, listener);
1249-
}
1256+
addUpdateOnListeners(scope, element, ctrl.$options, listener);
12501257

12511258
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
1252-
element.on('click', listener);
1259+
element.on('click', function(ev) {
1260+
scope.$apply(function() {
1261+
listener(ev);
1262+
});
1263+
});
12531264
}
12541265

12551266
ctrl.$render = function() {
@@ -1817,7 +1828,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18171828
: ctrl.$options.debounce) || 0;
18181829

18191830
$timeout.cancel(pendingDebounce);
1820-
if (debounceDelay) {
1831+
if (debounceDelay && trigger !== 'submit') {
18211832
pendingDebounce = $timeout(function() {
18221833
ctrl.$$realSetViewValue(value);
18231834
}, debounceDelay);
@@ -2264,6 +2275,11 @@ var ngValueDirective = function() {
22642275
* important because `form` controllers are published to the related scope under the name in their
22652276
* `name` attribute.
22662277
*
2278+
* Any pending changes will take place immediately when an enclosing form is submitted via the
2279+
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
2280+
* to have access to the updated model. It is possible to flush the pending changes manually by
2281+
* triggering a scope event with name `$updateInputModels`.
2282+
*
22672283
* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
22682284
* - `updateOn`: string specifying which event should be the input bound to. You can set several
22692285
* events using an space delimited list. There is a special event called `default` that
@@ -2358,13 +2374,13 @@ var ngModelOptionsDirective = function() {
23582374
var that = this;
23592375
this.$options = $scope.$eval($attrs.ngModelOptions);
23602376
// Allow adding/overriding bound events
2361-
if (this.$options.updateOn) {
2377+
if (this.$options.updateOn !== undefined) {
23622378
this.$options.updateOnDefault = false;
23632379
// extract "default" pseudo-event from list of events that can trigger a model update
2364-
this.$options.updateOn = this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
2380+
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
23652381
that.$options.updateOnDefault = true;
23662382
return ' ';
2367-
});
2383+
}));
23682384
} else {
23692385
this.$options.updateOnDefault = true;
23702386
}

test/ng/directive/inputSpec.js

+89
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,95 @@ describe('input', function() {
847847
dealoc(doc);
848848
}));
849849

850+
it('should trigger update on form submit', function() {
851+
var form = $compile(
852+
'<form name="test" ng-model-options="{ updateOn: \'\' }" >' +
853+
'<input type="text" ng-model="name" />' +
854+
'</form>')(scope);
855+
856+
var input = form.find('input').eq(0);
857+
input.val('a');
858+
expect(scope.name).toEqual(undefined);
859+
browserTrigger(form, 'submit');
860+
expect(scope.name).toEqual('a');
861+
dealoc(form);
862+
});
863+
864+
it('should flush debounced events when form is submitted', function() {
865+
var form = $compile(
866+
'<form name="test" ng-model-options="{ debounce: 1000 }" >' +
867+
'<input type="text" ng-model="name" />' +
868+
'</form>')(scope);
869+
870+
var input = form.find('input').eq(0);
871+
input.val('a');
872+
expect(scope.name).toEqual(undefined);
873+
browserTrigger(form, 'submit');
874+
expect(scope.name).toEqual('a');
875+
dealoc(form);
876+
});
877+
878+
it('should flush debounced events on $updateInputModels scope event', function() {
879+
var input = $compile(
880+
'<input type="text" ng-model="name" ' +
881+
'ng-model-options="{ debounce: 1000 }" />')(scope);
882+
883+
input.val('a');
884+
expect(scope.name).toEqual(undefined);
885+
scope.$apply(function () {
886+
scope.$broadcast('$updateInputModels');
887+
expect(scope.name).toEqual('a');
888+
});
889+
dealoc(input);
890+
});
891+
892+
it('should trigger update of checkbox on $updateInputModels', function() {
893+
var input = $compile(
894+
'<input type="checkbox" ng-model="name" ' +
895+
'ng-model-options="{ debounce: 1000 }" />')(scope);
896+
scope.$digest();
897+
898+
browserTrigger(input, 'click');
899+
expect(scope.name).toEqual(undefined);
900+
scope.$apply(function () {
901+
scope.$broadcast('$updateInputModels');
902+
expect(scope.name).toEqual(true);
903+
});
904+
dealoc(input);
905+
});
906+
907+
it('should trigger update of radio buttons on $updateInputModels', function() {
908+
var input = $compile(
909+
'<input type="radio" ng-model="name" value="me" ' +
910+
'ng-model-options="{ debounce: 1000 }" />')(scope);
911+
scope.$digest();
912+
913+
browserTrigger(input, 'click');
914+
expect(scope.name).toEqual(undefined);
915+
scope.$apply(function () {
916+
scope.$broadcast('$updateInputModels');
917+
expect(scope.name).toEqual('me');
918+
});
919+
dealoc(input);
920+
});
921+
922+
it('should trigger update before ng-submit is invoked', function() {
923+
var form = $compile(
924+
'<form name="test" ng-submit="submit()" ' +
925+
'ng-model-options="{ updateOn: \'\' }" >' +
926+
'<input type="text" ng-model="name" />' +
927+
'</form>')(scope);
928+
929+
var input = form.find('input').eq(0);
930+
input.val('a');
931+
scope.submit = jasmine.createSpy('submit').andCallFake(function() {
932+
expect(scope.name).toEqual('a');
933+
});
934+
browserTrigger(form, 'submit');
935+
expect(scope.submit).toHaveBeenCalled();
936+
dealoc(form);
937+
});
938+
850939
it('should allow canceling pending updates', inject(function($timeout) {
851940
compileInput(
852941
'<input type="text" ng-model="name" name="alias" '+

0 commit comments

Comments
 (0)