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

feat(ngModelOptions): support submit trigger #7094

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/ng/directive/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ function FormController(element, attrs, $scope, $animate) {
* hitting enter in any of the input fields will trigger the click handler on the *first* button or
* input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
*
* Note that any pending `ngModelOptions` changes will take place immediately when an enclosing form
* is submitted thanks to `ngForm` broadcasting a scope event with name `$updateInputModels`.
* Use `ngSubmit` to have access to the updated model since `ngClick` events will occur before the
* model is updated.
*
* @param {string=} name Name of the form. If specified, the form controller will be published into
* related scope, under this name.
*
Expand Down Expand Up @@ -381,19 +386,23 @@ var formDirectiveFactory = function(isNgForm) {
// IE 9 is not affected because it doesn't fire a submit event and try to do a full
// page reload if the form was destroyed by submission of the form via a click handler
// on a button in the form. Looks like an IE9 specific bug.
var preventDefaultListener = function(event) {
var handleFormSubmission = function(event) {
scope.$apply(function() {
scope.$broadcast('$updateInputModels', event);
});

event.preventDefault
? event.preventDefault()
: event.returnValue = false; // IE
};

addEventListenerFn(formElement[0], 'submit', preventDefaultListener);
addEventListenerFn(formElement[0], 'submit', handleFormSubmission);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not keen on function names containing the word "and" it implies the function is not cohesive. How about handleFormSubmission()?

// unregister the preventDefault listener so that we don't not leak memory but in a
// way that will achieve the prevention of the default action.
formElement.on('$destroy', function() {
$timeout(function() {
removeEventListenerFn(formElement[0], 'submit', preventDefaultListener);
removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
}, 0, false);
});
}
Expand Down
72 changes: 44 additions & 28 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)$/;
var DEFAULT_REGEXP = /(\b|^)default(\b|$)/;
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;

var inputType = {

Expand Down Expand Up @@ -878,6 +878,25 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
}
}

function addUpdateOnListeners(scope, element, options, listener) {
if (options) {
if (options.updateOn) {
element.on(options.updateOn, function(ev) {
scope.$apply(function() {
listener(ev);
});
});
}

scope.$on('$updateInputModels', function(scopeEvent, ev) {
// Since this event can be triggered manually, we pass a dummy submit event
// in case no 'ev' argument is passed. This is important since $setViewValue
// will never debounce stuff that come from 'submit' trigger.
listener(ev || {type: 'submit'});
});
}
}

function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var validity = element.prop('validity');

Expand Down Expand Up @@ -924,11 +943,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
};

// Allow adding/overriding bound events
if (ctrl.$options && ctrl.$options.updateOn) {
// bind to user-defined events
element.on(ctrl.$options.updateOn, listener);
}
addUpdateOnListeners(scope, element, ctrl.$options, listener);

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

var listener = function(ev) {
if (element[0].checked) {
scope.$apply(function() {
ctrl.$setViewValue(attr.value, ev && ev.type);
});
ctrl.$setViewValue(attr.value, ev && ev.type);
}
};

// Allow adding/overriding bound events
if (ctrl.$options && ctrl.$options.updateOn) {
// bind to user-defined events
element.on(ctrl.$options.updateOn, listener);
}
addUpdateOnListeners(scope, element, ctrl.$options, listener);

if (!ctrl.$options || ctrl.$options.updateOnDefault) {
element.on('click', listener);
element.on('click', function(ev) {
scope.$apply(function() {
listener(ev);
});
});
}

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

var listener = function(ev) {
scope.$apply(function() {
ctrl.$setViewValue(element[0].checked, ev && ev.type);
});
ctrl.$setViewValue(element[0].checked, ev && ev.type);
};

// Allow adding/overriding bound events
if (ctrl.$options && ctrl.$options.updateOn) {
// bind to user-defined events
element.on(ctrl.$options.updateOn, listener);
}
addUpdateOnListeners(scope, element, ctrl.$options, listener);

if (!ctrl.$options || ctrl.$options.updateOnDefault) {
element.on('click', listener);
element.on('click', function(ev) {
scope.$apply(function() {
listener(ev);
});
});
}

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

$timeout.cancel(pendingDebounce);
if (debounceDelay) {
if (debounceDelay && trigger !== 'submit') {
pendingDebounce = $timeout(function() {
ctrl.$$realSetViewValue(value);
}, debounceDelay);
Expand Down Expand Up @@ -2264,6 +2275,11 @@ var ngValueDirective = function() {
* important because `form` controllers are published to the related scope under the name in their
* `name` attribute.
*
* Any pending changes will take place immediately when an enclosing form is submitted via the
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
* to have access to the updated model. It is possible to flush the pending changes manually by
* triggering a scope event with name `$updateInputModels`.
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this information should also appear (in some form[sic]) in the form directive documentation too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
* - `updateOn`: string specifying which event should be the input bound to. You can set several
* events using an space delimited list. There is a special event called `default` that
Expand Down Expand Up @@ -2358,13 +2374,13 @@ var ngModelOptionsDirective = function() {
var that = this;
this.$options = $scope.$eval($attrs.ngModelOptions);
// Allow adding/overriding bound events
if (this.$options.updateOn) {
if (this.$options.updateOn !== undefined) {
this.$options.updateOnDefault = false;
// extract "default" pseudo-event from list of events that can trigger a model update
this.$options.updateOn = this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
that.$options.updateOnDefault = true;
return ' ';
});
}));
} else {
this.$options.updateOnDefault = true;
}
Expand Down
89 changes: 89 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,95 @@ describe('input', function() {
dealoc(doc);
}));

it('should trigger update on form submit', function() {
var form = $compile(
'<form name="test" ng-model-options="{ updateOn: \'\' }" >' +
'<input type="text" ng-model="name" />' +
'</form>')(scope);

var input = form.find('input').eq(0);
input.val('a');
expect(scope.name).toEqual(undefined);
browserTrigger(form, 'submit');
expect(scope.name).toEqual('a');
dealoc(form);
});

it('should flush debounced events when form is submitted', function() {
var form = $compile(
'<form name="test" ng-model-options="{ debounce: 1000 }" >' +
'<input type="text" ng-model="name" />' +
'</form>')(scope);

var input = form.find('input').eq(0);
input.val('a');
expect(scope.name).toEqual(undefined);
browserTrigger(form, 'submit');
expect(scope.name).toEqual('a');
dealoc(form);
});

it('should flush debounced events on $updateInputModels scope event', function() {
var input = $compile(
'<input type="text" ng-model="name" ' +
'ng-model-options="{ debounce: 1000 }" />')(scope);

input.val('a');
expect(scope.name).toEqual(undefined);
scope.$apply(function () {
scope.$broadcast('$updateInputModels');
expect(scope.name).toEqual('a');
});
dealoc(input);
});

it('should trigger update of checkbox on $updateInputModels', function() {
var input = $compile(
'<input type="checkbox" ng-model="name" ' +
'ng-model-options="{ debounce: 1000 }" />')(scope);
scope.$digest();

browserTrigger(input, 'click');
expect(scope.name).toEqual(undefined);
scope.$apply(function () {
scope.$broadcast('$updateInputModels');
expect(scope.name).toEqual(true);
});
dealoc(input);
});

it('should trigger update of radio buttons on $updateInputModels', function() {
var input = $compile(
'<input type="radio" ng-model="name" value="me" ' +
'ng-model-options="{ debounce: 1000 }" />')(scope);
scope.$digest();

browserTrigger(input, 'click');
expect(scope.name).toEqual(undefined);
scope.$apply(function () {
scope.$broadcast('$updateInputModels');
expect(scope.name).toEqual('me');
});
dealoc(input);
});

it('should trigger update before ng-submit is invoked', function() {
var form = $compile(
'<form name="test" ng-submit="submit()" ' +
'ng-model-options="{ updateOn: \'\' }" >' +
'<input type="text" ng-model="name" />' +
'</form>')(scope);

var input = form.find('input').eq(0);
input.val('a');
scope.submit = jasmine.createSpy('submit').andCallFake(function() {
expect(scope.name).toEqual('a');
});
browserTrigger(form, 'submit');
expect(scope.submit).toHaveBeenCalled();
dealoc(form);
});

it('should allow canceling pending updates', inject(function($timeout) {
compileInput(
'<input type="text" ng-model="name" name="alias" '+
Expand Down