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

Commit 50ceb23

Browse files
petebacondarwinNarretz
authored andcommitted
fix(ngMessages): prevent memory leak from messages that are never attached
Closes #16389 Closes #16404 Closes #16406
1 parent ab386cd commit 50ceb23

File tree

2 files changed

+37
-7
lines changed

2 files changed

+37
-7
lines changed

src/ngMessages/messages.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -417,13 +417,6 @@ angular.module('ngMessages', [], function initAngularHelpers() {
417417

418418
$scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render);
419419

420-
// If the element is destroyed, proactively destroy all the currently visible messages
421-
$element.on('$destroy', function() {
422-
forEach(messages, function(item) {
423-
item.message.detach();
424-
});
425-
});
426-
427420
this.reRender = function() {
428421
if (!renderLater) {
429422
renderLater = true;
@@ -498,6 +491,9 @@ angular.module('ngMessages', [], function initAngularHelpers() {
498491
function removeMessageNode(parent, comment, key) {
499492
var messageNode = messages[key];
500493

494+
// This message node may have already been removed by a call to deregister()
495+
if (!messageNode) return;
496+
501497
var match = findPreviousMessage(parent, comment);
502498
if (match) {
503499
match.next = messageNode.next;
@@ -702,6 +698,8 @@ function ngMessageDirectiveFactory() {
702698
// by another structural directive then it's time
703699
// to deregister the message from the controller
704700
currentElement.on('$destroy', function() {
701+
// If the message element was removed via a call to `detach` then `currentElement` will be null
702+
// So this handler only handles cases where something else removed the message element.
705703
if (currentElement && currentElement.$$attachId === $$attachId) {
706704
ngMessagesCtrl.deregister(commentNode);
707705
messageCtrl.detach();
@@ -719,6 +717,14 @@ function ngMessageDirectiveFactory() {
719717
}
720718
}
721719
});
720+
721+
// We need to ensure that this directive deregisters itself when it no longer exists
722+
// Normally this is done when the attached element is destroyed; but if this directive
723+
// gets removed before we attach the message to the DOM there is nothing to watch
724+
// in which case we must deregister when the containing scope is destroyed.
725+
scope.$on('$destroy', function() {
726+
ngMessagesCtrl.deregister(commentNode);
727+
});
722728
}
723729
};
724730
}];

test/ngMessages/messagesSpec.js

+24
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,30 @@ describe('ngMessages', function() {
636636
})
637637
);
638638

639+
it('should unregister the ngMessage even if it was never attached',
640+
inject(function($compile, $rootScope) {
641+
var html =
642+
'<div ng-messages="items">' +
643+
'<div ng-if="show"><div ng-message="x">ERROR</div></div>' +
644+
'</div>';
645+
646+
element = $compile(html)($rootScope);
647+
648+
var ctrl = element.controller('ngMessages');
649+
650+
expect(messageChildren(element).length).toBe(0);
651+
expect(Object.keys(ctrl.messages).length).toEqual(0);
652+
653+
$rootScope.$apply('show = true');
654+
expect(messageChildren(element).length).toBe(0);
655+
expect(Object.keys(ctrl.messages).length).toEqual(1);
656+
657+
$rootScope.$apply('show = false');
658+
expect(messageChildren(element).length).toBe(0);
659+
expect(Object.keys(ctrl.messages).length).toEqual(0);
660+
})
661+
);
662+
639663

640664
describe('when including templates', function() {
641665
they('should work with a dynamic collection model which is managed by ngRepeat',

0 commit comments

Comments
 (0)