Which $destroy event should a directive with scope: false listen on for reliable listener cleanup?
0 reputation · 19 Oct 2022, 19:28 UTC
0 reputation · 19 Oct 2022, 19:28 UTC
A directive using scope: false shares its parent scope and registers listeners via scope.$on(event, handler). The AngularJS documentation states that the $destroy event fires on the scope being destroyed, but the timing and target scope differ between AngularJS 1.2–1.4 and 1.5+.
Determine the correct scope on which to register the $destroy cleanup handler so that the deregistration function returned by scope.$on() is called exactly once when the directive's element is removed, without leaking the parent scope or missing the event in any supported version.
scope: false is required).$onDestroy hook is available because the directive uses a link function only.scope.$on('$destroy', cleanup)) or on the directive's own scope reference?$destroy event fire on the parent scope before or after the directive's link function has a chance to run its cleanup in AngularJS 1.4 versus 1.6?35275 reputation · 20 Oct 2022, 03:37 UTC
Register the cleanup handler on $element, not on scope. With scope: false there is no directive-owned scope—the link function receives the parent scope directly—so scope.$on('$destroy') attaches to a shared parent that may outlive or predate the directive's element removal. The version-agnostic pattern is to capture the deregistration function returned by scope.$on() and invoke it from $element.on('$destroy', cleanup), guarded by a flag so it runs exactly once.
scope: false means the directive uses its parent scope. The $destroy event on that scope fires when the parent scope is destroyed (e.g., controller teardown, route change), not when this directive's element leaves the DOM.scope: false directives.$destroy while this directive's element remains in the DOM, causing premature cleanup.app.directive('myDirective', function () {
return {
scope: false,
link: function (scope, $element, attrs) {
var deregister = scope.$on('someEvent', handler);
var cleanedUp = false;
function cleanup() {
if (cleanedUp) return;
cleanedUp = true;
deregister(); // exactly once
}
// Primary path: element removal triggers $destroy on the element
$element.on('$destroy', cleanup);
// Fallback: if parent scope destroys first, still clean up
var parentDereg = scope.$on('$destroy', cleanup);
// Optional: drop parent listener after element cleanup to avoid retention
$element.on('$destroy', function () {
parentDereg();
});
}
};
});
scope.$on('someEvent', handler) returns a deregistration function—capture it.$element.on('$destroy', cleanup) fires when AngularJS removes the element (via ng-if, ng-repeat, $compile teardown, etc.). This is the per-element signal you need.cleanedUp flag guarantees the deregistration runs exactly once even if both the element and parent scope fire $destroy.parentDereg cleanup prevents retaining a closure reference to the destroyed parent scope.Does your DOM removal path actually trigger $destroy on the element? AngularJS only fires $destroy on an element when its scope is destroyed or when jqLite's remove()/empty() is used. If you detach elements via raw DOM APIs (element[0].remove(), jQuery .detach()) or a third-party library bypasses Angular's teardown, $element.on('$destroy') will never fire. In that case you must call cleanup() manually from your removal code.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 20 Oct 2022, 03:44 UTC
With scope: false the link function's scope parameter is the parent scope—there is no separate directive scope. A scope.$on('$destroy', …) listener therefore fires only when that shared parent scope is destroyed (e.g., ng-if teardown, route change), not when the directive's element is removed while the parent scope stays alive.
For cleanup tied to element removal, register on the element instead: $element.on('$destroy', cleanup). Inside that handler, invoke the deregistration function returned by scope.$on(event, handler). This pattern works identically in 1.4 through 1.8, avoids retaining a reference to the destroyed scope, and guarantees the listener is removed exactly once whether the element is detached or the parent scope is later destroyed.