0

I have a spreadsheet-like table setup built with table elements that have use angular to enable editing, including a row select/delete function. This is the html that enables a row to be highlighted if its "selected":

    <tr row-id="0" ng-class="selectedClass(5)" class="ng-scope budget-row">
      <td>
        <label id="foo" ng-hide="some_function(5)">{{row(5).value}}</label>
        <input type="text" ng-hide="!some_function(5)">
      </td>
      <td>...</td>
    </tr>

The ng-class="selectedClass(5)" is used to apply a background color to the row when selected.

Once a row is selected, it can be "cut". The cut method is called on the scope, and it removes the underlying row object and removes the element from the Dom as follows:

 let el = angular.element("#foo");
 let deleteTarget = el.parent().parent();
 deleteTarget.remove();

This all works fine on the page, but I can see on the console and the stack trace, that ng-class directive is still apparently firing on the removed rows even after they are removed from the page. Although it works fine now, I can see it becoming very slow if all the rows that are deleted are still being checked for changes by angular even if they are no longer present on the page.

Here's the stack trace where the "isSelected" function is being called from a element that has already been removed.

isSelected (budget_items.self-e32480dfbdba1522e84f3891a1bef58643f758332acf63bc6a7e4b06bfee6b49.js?body=1:688)
$scope.selectedClass (budget_items.self-e32480dfbdba1522e84f3891a1bef58643f758332acf63bc6a7e4b06bfee6b49.js?body=1:50)
fn (VM700:4)
expressionInputWatch (angular.self-78d77e4a3c91d5788f4be38711bc43955307738d52116f065bc1289f6abc3523.js?body=1:16661)
$digest (angular.self-78d77e4a3c91d5788f4be38711bc43955307738d52116f065bc1289f6abc3523.js?body=1:18364)
$apply (angular.self-78d77e4a3c91d5788f4be38711bc43955307738d52116f065bc1289f6abc3523.js?body=1:18641)
(anonymous) (angular.self-78d77e4a3c91d5788f4be38711bc43955307738d52116f065bc1289f6abc3523.js?body=1:27469)
dispatch (jquery3.self-5af507e253c37e9c9dcf65064fc3f93795e6e28012780579975a4d709f4074ad.js?body=1:5184)
elemData.handle (jquery3.self-5af507e253c37e9c9dcf65064fc3f93795e6e28012780579975a4d709f4074ad.js?body=1:4992)
georgeawg
  • 48,608
  • 13
  • 72
  • 95
GGizmos
  • 3,443
  • 4
  • 27
  • 72

1 Answers1

0

AngularJs use special event named: $destroy to remove watchers and make all that clean-up stuff. When you use jQuery, Angular will not be notified that something should be removed. That's the root of the problem.

So you have two options:

  1. Use standard AngularJs tools like ng-if
  2. Create a bridge directive that will catch remove event and notify AngularJs about this. How to work with $destroy described here. How to handle DOM node removed event described here.

And yes, mixing AngularJs and jQuery or even native DOM API will bring you dozen of problems

Drag13
  • 5,859
  • 1
  • 18
  • 42
  • I thought about using ng-if, but it seemed to me that in order for ng-if to work correctly, it must be evaluating the condition even after I no longer need the row. What I am doing is essentially an editing program, making things disappear by setting the expression defined in ng-if to false means the deleted rows hang around forever just in case ng-if evaluates to true at some point. That seems suboptimal, as I will have a lot of garbage rows sitting around in memory somewhere being evaluated on every cycle even though they have been disposed of. What am I mis understanding? – GGizmos Jun 23 '18 at 16:08
  • You are correct. Working with dynamic templates are not that easy. I can offer to create an angular wrapper. Outside of the wrapper you will pass data and all angular staff, but inside you will deal with jQury. That will help you separate both approaches. I would try this way. – Drag13 Jun 23 '18 at 17:48
  • Any examples would be very useful. I can't seem to figure out how to work with destroy as demonstrated in the links since I am creating each row by compilng an html string inserting in the dom. The examples seem to suggest destroying the "scope of the element removed" but in my case each row gets its values from an object on the rootscope (a 'row' from a collection rows maintained by a budget object. with an expression like {{budget.row(6).subtotal}} and similar. So how do I get rid of all these watchers created by the compile step? – GGizmos Jun 23 '18 at 19:15
  • Oh, it's quite easy, use {{::budget.row(6).subtotal}} to create one-time binding. This will evaluate an expression only once and destroy watcher just right after. – Drag13 Jun 23 '18 at 19:23
  • Well that's a start, but the row recalculates after changes to other cells in the row. What I want to do is destroy the binding on removing. Any clue how to do that? – GGizmos Jun 23 '18 at 21:48
  • Try to listen to remove node event, and broadcast $destroy angular event. – Drag13 Jun 24 '18 at 03:28
  • I decided to refactor the whole thing with ng-repeat. Hopefully when I delete items from the collection ng-repeat is looking at, all the elments previously created and now missing from the collection will be destroyed. However, I would really like to unjderstand how to broadcast $destroy angular event. Do I just broadcast that to everything or to the specific object destoryed or what? Can't really figure out how to do that because all the examples I see call against $destroy against a specific scope, and I only have root scope in my project. – GGizmos Jun 24 '18 at 16:55
  • You all most can't have only root scope even if you have only one component or controller. Even ng repeat creates it's own scope. You can check your scope tree inspecting your rootScope children elements. Main idea for excluding memory leaks sounds like: don't destroy yourself anything created by angular. – Drag13 Jun 25 '18 at 05:35