Angular ng-if and ng-show combination

Imagine some heavy content that might be rendered on a web page, such as a chart.
Angular gives 2 options to toggle the visibility of said content.

ng-show will render the content regardless of the expression and simple “hide” it after the fact. This is not ideal since the user may never “open” the content during their session, so it was a waste to render it.

ng-if is better in this regard. Using it in place of ng-show will prevent the heavy content from being rendered in the first place if the expression is false. However, its strength is also its weakness, because if the user hides the chart and then shows it again, the content is rendered from scratch each time.

How can I make a directive that takes the best of both worlds? Meaning it works like ng-if until the content is rendered the first time, then switches to work like ng-show to prevent re-rendering it each time.

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

+1 on Denis’s answer, but just for completeness-sake, it can even be simplified further by keeping the logic in the View without “polluting” the controller:

<button ng-click="show = !show">toggle</button>
<div ng-if="once = once || show" ng-show="show">Heavy content</div>

plunker

EDIT: the version above could be further improved (and simplified) with one-time binding to reduce an unnecessary $watch on once – this will only work in Angular 1.3+:

<div ng-if="::show || undefined" ng-show="show">Heavy content</div>

The undefined is needed to ensure that the watched value does not “stabilize” before it becomes true. Once it stabilizes, it also loses the $watch, so it would not be impacted by any further change to show.

Solution 2

You could make it work by using ngIf and ngShow together, where each one is controller by different variable. ngIf is going to be set to true once and never set to false again, while ngShow will be toggled as much as the user wants to.

Take a look at this fiddle.

Solution 3

It was @new-dev ‘s answer that inspired my own combination idea to get a quite heavy component fast using a toggle button.

My original problem was that my page consists of ~20 components each taking ~1 second to load. Each being toggled by a button.

If using plain ng-if I would get instant page loading and 1 sec delay after a toggle press.

If using plain ng-show I would get instant toggle presses but 20 second page loading delay…

So, now I combine them with a ng-mouseenter control too. Like this.

<div ng-mouseenter="create=true">
    <button ng-click="showAll = !showAll"></button>
    <!--lightweight content-->
    <div ng-show="showAll">
        <div ng-if="create">
            <!--Heavy content-->
        </div>
    </div>
</div>

This gave me superb performance in both chrome and IE. The time it takes for the user to actually click the button after entering the component is used for the DOM creation. And then the DOM nodes are quickly displayed once (if) the click arrives.

I also never set the ng-if expression to false again. Keeping the DOM cached if the user keeps toggle that section.

Update:
The reason I do not have the ng-if and ng-show on the same div is that in IE it caused flickering. When mouseenter event arrived it would show the content and then hide it. In chrome it was ok to have it on the same div.

Solution 4

You may be interested in a custom directive ng-lazy-show by Alan Colver. It allows combining both ng-if and ng-show:

<div ng-lazy-show="showFilters" lendio-business-filters></div>

The code is small and can easily be added to your project:

var ngLazyShowDirective = ['$animate', function ($animate) {

  return {
    multiElement: true,
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    link: function ($scope, $element, $attr, $ctrl, $transclude) {
      var loaded;
      $scope.$watch($attr.ngLazyShow, function ngLazyShowWatchAction(value) {
        if (loaded) {
          $animate[value ? 'removeClass' : 'addClass']($element, 'ng-hide');
        }
        else if (value) {
          loaded = true;
          $transclude(function (clone) {
            clone[clone.length++] = document.createComment(' end ngLazyShow: ' + $attr.ngLazyShow + ' ');
            $animate.enter(clone, $element.parent(), $element);
            $element = clone;
          });
        }
      });
    }
  };

}];

angular.module('yourModule').directive('ngLazyShow', ngLazyShowDirective);

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply