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

Commit 7295c60

Browse files
fix(ngMessages): prevent race condition with ngAnimate
If `ngMessage` tried to add a message back in that was about to be removed after an animation, the NgMessageController got confused and tried to detach the newly added message, when the pending node was destroyed. This change applies a unique `attachId` to the message object and its DOM node when it is attached. This is then checked when a DOM node is being destroyed to prevent unwanted calls to `detach`. Closes #12856 Closes #12903
1 parent fa01571 commit 7295c60

File tree

2 files changed

+52
-1
lines changed

2 files changed

+52
-1
lines changed

src/ngMessages/messages.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,9 @@ angular.module('ngMessages', [])
325325
controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
326326
var ctrl = this;
327327
var latestKey = 0;
328+
var nextAttachId = 0;
329+
330+
this.getAttachId = function getAttachId() { return nextAttachId++; };
328331

329332
var messages = this.messages = {};
330333
var renderLater, cachedCollection;
@@ -636,11 +639,15 @@ function ngMessageDirectiveFactory(restrict) {
636639
$animate.enter(elm, null, element);
637640
currentElement = elm;
638641

642+
// Each time we attach this node to a message we get a new id that we can match
643+
// when we are destroying the node later.
644+
var $$attachId = currentElement.$$attachId = ngMessagesCtrl.getAttachId();
645+
639646
// in the event that the parent element is destroyed
640647
// by any other structural directive then it's time
641648
// to deregister the message from the controller
642649
currentElement.on('$destroy', function() {
643-
if (currentElement) {
650+
if (currentElement && currentElement.$$attachId === $$attachId) {
644651
ngMessagesCtrl.deregister(commentNode);
645652
messageCtrl.detach();
646653
}

test/ngMessages/messagesSpec.js

+44
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,50 @@ describe('ngMessages', function() {
372372
expect(trim(element.text())).toEqual("Enter something");
373373
}));
374374

375+
// issue #12856
376+
it('should only detach the message object that is associated with the message node being removed',
377+
inject(function($rootScope, $compile, $animate) {
378+
379+
// We are going to spy on the `leave` method to give us control over
380+
// when the element is actually removed
381+
spyOn($animate, 'leave');
382+
383+
// Create a basic ng-messages set up
384+
element = $compile('<div ng-messages="col">' +
385+
' <div ng-message="primary">Enter something</div>' +
386+
'</div>')($rootScope);
387+
388+
// Trigger the message to be displayed
389+
$rootScope.col = { primary: true };
390+
$rootScope.$digest();
391+
expect(messageChildren(element).length).toEqual(1);
392+
var oldMessageNode = messageChildren(element)[0];
393+
394+
// Remove the message
395+
$rootScope.col = { primary: undefined };
396+
$rootScope.$digest();
397+
398+
// Since we have spied on the `leave` method, the message node is still in the DOM
399+
expect($animate.leave).toHaveBeenCalledOnce();
400+
var nodeToRemove = $animate.leave.mostRecentCall.args[0][0];
401+
expect(nodeToRemove).toBe(oldMessageNode);
402+
$animate.leave.reset();
403+
404+
// Add the message back in
405+
$rootScope.col = { primary: true };
406+
$rootScope.$digest();
407+
408+
// Simulate the animation completing on the node
409+
jqLite(nodeToRemove).remove();
410+
411+
// We should not get another call to `leave`
412+
expect($animate.leave).not.toHaveBeenCalled();
413+
414+
// There should only be the new message node
415+
expect(messageChildren(element).length).toEqual(1);
416+
var newMessageNode = messageChildren(element)[0];
417+
expect(newMessageNode).not.toBe(oldMessageNode);
418+
}));
375419

376420
it('should render animations when the active/inactive classes are added/removed', function() {
377421
module('ngAnimate');

0 commit comments

Comments
 (0)