1

I have a custom AngularJS component which might be used on a single web page over 200 times. The page ends up implementing over 4000 watchers -- which is more than AngularJS's prefered maximum amount of watchers -- and makes the page really slow.

The actual problem is that there is a lot of unneeded watchers left from some ng-if and other AngularJS expressions inside the component template which no longer where going to change their values.

For normal ng-if's the fix was easy:

<div ng-if="::$ctrl.isInitialized()">Ready!</div>

...where $ctrl.isInitialized() would either return a true (when the component was initialized) or undefined (until it was).

Returning undefined here will make AngularJS keep the watcher active until it returns something else, in this case the value true, and then will add the div in the DOM.

There is no ng-not="expression" like there is ng-hide. This works well with ng-hide, except of course the div is still in the DOM after the controller has been initialized, which is not the perfect solution.

But how can you implement it so, that the <div> will be in the DOM until the controller has been initialized and will be removed after?

2
  • stackoverflow.com/questions/40999224/… ? Commented Aug 30, 2018 at 17:48
  • @Ctznkane525 I didn't see anything about one time binding there. <div ng-if="!$ctrl.isInitialized()"></div> would of course remove the div, but would also keep an useless watcher active in the scope. I don't know how to do the same trick as with undefined, since returning false would also remove the watcher. Commented Aug 30, 2018 at 17:57

2 Answers 2

0

Although there is no ng-not directive, it was easy to implement from AngularJS source code:

var ngNotDirective = ['$animate', '$compile', function($animate, $compile) {

  function getBlockNodes(nodes) {
    // TODO(perf): update `nodes` instead of creating a new object?
    var node = nodes[0];
    var endNode = nodes[nodes.length - 1];
    var blockNodes;

    for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
      if (blockNodes || nodes[i] !== node) {
        if (!blockNodes) {
          blockNodes = jqLite(slice.call(nodes, 0, i));
        }
        blockNodes.push(node);
      }
    }

    return blockNodes || nodes;
  }

  return {
    multiElement: true,
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    $$tlb: true,
    link: function($scope, $element, $attr, ctrl, $transclude) {
      var block, childScope, previousElements;
      $scope.$watch($attr.ngNot, function ngNotWatchAction(value) {
        if (!value) {
          if (!childScope) {
            $transclude(function(clone, newScope) {
              childScope = newScope;
              clone[clone.length++] = $compile.$$createComment('end ngNot', $attr.ngNot);
              // Note: We only need the first/last node of the cloned nodes.
              // However, we need to keep the reference to the jqlite wrapper as it might be changed later
              // by a directive with templateUrl when its template arrives.
              block = {
                clone: clone
              };
              $animate.enter(clone, $element.parent(), $element);
            });
          }
        } else {
          if (previousElements) {
            previousElements.remove();
            previousElements = null;
          }
          if (childScope) {
            childScope.$destroy();
            childScope = null;
          }
          if (block) {
            previousElements = getBlockNodes(block.clone);
            $animate.leave(previousElements).done(function(response) {
              if (response !== false) previousElements = null;
            });
            block = null;
          }
        }
      });
    }
  };
}];

This is the same implementation as ng-if except it has reverted if (!value) check.

It can be used like this:

<div ng-not="::$ctrl.isInitialized() ? true : undefined">Loading...</div>

It is easy to verify that there is no useless watchers by adding a console.log() in $ctrl.isInitialized() -- this function will be called just few times until it returns true and the watcher is removed -- as well as the div, and anything inside it.

Sign up to request clarification or add additional context in comments.

Comments

0

kind of quick patch: angular allows ternary operator in expressions after v1.1.5 I guess.

So you can make something like:

<div ng-if="::$ctrl.isInitialized() === undefined? undefined: !$ctrl.isInitialized()">

As far as I can see undefined does not have special meaning in angular expression - it's treated as another (not defined yet) variable in $scope. So I had to put it there explicitly:

$scope = undefined;

Alternative option is writing short helper:

function isDefined(val) {
    return angular.isDefined(val) || undefined;
}

To use it later as

ng-if="::isDefined($ctrl.isInitialized()) && !$ctrl.isInitialized()"

But since you say there are too many places for doing that - for sure making own component as you coded above looks better

6 Comments

I couldn't figure out how this could possible work. Here's my test: next.plnkr.co/edit/TMkAo4DFgKwANzet
@jheusala you are referencing to $ctrl. in ng-if while you are actually setting methods/data into $scope. Check version with fix: next.plnkr.co/edit/OsWZK17JJgwu4m7x - also I've replaced =true with =false to see it in action up to your need
I may have missed something but your example does not keep the Loading 1 & 2 in the DOM until it has a value. Yes, it "removes" it when it finally has a value, but since the div will never be in the DOM, it is pretty useless.
just realized you need "keep something until component is initialized". I thought the main goal is to keep using negation in ng-if(because of better readability or something like that) but getting one-time binding and performance boost it gives. am I right on this assumption?
Main goal is to remove a div and all its children (including any other watchers in child components) from the DOM after something changes state. Eg, the reverse of what you can archive with ng-if and one time bind for the initialized main content; usually this is a loading spinner or content something like that. This is also why a solution which just changes text in the DOM is not perfect solution.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.