71

I have an angular template which looks like this...

<div ng-repeat="message in data.messages" ng-class="message.type">

    <div class="info">
        <div class="type"></div>
        <div class="from">From Avatar</div>
        <div class="createdBy">Created By Avatar</div>
        <div class="arrowTo">
            <div class="arrow"></div>
            <div class="to">To Avatar</div>
        </div>
        <div class="date">
            <div class="day">25</div>
            <div class="month">Dec</div>
        </div>
    </div>

    <div class="main">
        <div class="content">
            <div class="heading2">{{message.title}}</div>
            <div ng-bind-html="message.content"></div>
        </div>

    </div>

    <br />
    <hr />
    <br />

</div>

I have set up a JSfiddle to show the data being bound.

What I need to do is make the "from", "to" and "arrowTo" divs show conditionally, depending on the content of the data.

The log is is this...

  • If there is a "from" object in the data then show the "from" div and bind the data but don't show the "createdBy" div .
  • If there is no "from" object but there is a "createdBy" object then show the "createdBy" div and bind the data.
  • If there is a "to" object in the data then show the "arrowTo" div and bind it's data.

Or in plain English, if there is a from address, show it, otherwise show who created the record instead and if there is a to address then show that too.

I have looked into using ng-switch but I think I'd have to add extra markup which would leave an empty div if there was no data. Plus I'd need to nest switch directives and I'm not sure if that would work.

Any ideas?

UPDATE:

If I were to write my own directive (If I knew how!) then here is some pseudo code to show how I would want to use it...

<div ng-if="showFrom()">
    From Template Goes Here
</div>
<div ng-if="showCreatedBy()">
    CreatedBy Template Goes Here
</div>
<div ng-if="showTo()">
    To Template Goes Here
</div>

Each of these would disappear if the function/expression evaluated to false.

MarioDS
  • 12,895
  • 15
  • 65
  • 121
jonhobbs
  • 26,684
  • 35
  • 115
  • 170
  • I had more or less the same question and I ended up using Angular UI and their UI-IF implementation. It removed what I wanted from the DOM and therefore the issue was solved. More info is available here: http://angular-ui.github.com/ – alainvd Dec 29 '12 at 19:24

3 Answers3

89

Angular 1.1.5 introduced the ng-if directive. That's the best solution for this particular problem. If you are using an older version of Angular, consider using angular-ui's ui-if directive.

If you arrived here looking for answers to the general question of "conditional logic in templates" also consider:


Original answer:

Here is a not-so-great "ng-if" directive:

myApp.directive('ngIf', function() {
    return {
        link: function(scope, element, attrs) {
            if(scope.$eval(attrs.ngIf)) {
                // remove '<div ng-if...></div>'
                element.replaceWith(element.children())
            } else {
                element.replaceWith(' ')
            }
        }
    }
});

that allows for this HTML syntax:

<div ng-repeat="message in data.messages" ng-class="message.type">
   <hr>
   <div ng-if="showFrom(message)">
       <div>From: {{message.from.name}}</div>
   </div>    
   <div ng-if="showCreatedBy(message)">
      <div>Created by: {{message.createdBy.name}}</div>
   </div>    
   <div ng-if="showTo(message)">
      <div>To: {{message.to.name}}</div>
   </div>    
</div>

Fiddle.

replaceWith() is used to remove unneeded content from the DOM.

Also, as I mentioned on Google+, ng-style can probably be used to conditionally load background images, should you want to use ng-show instead of a custom directive. (For the benefit of other readers, Jon stated on Google+: "both methods use ng-show which I'm trying to avoid because it uses display:none and leaves extra markup in the DOM. This is a particular problem in this scenario because the hidden element will have a background image which will still be loaded in most browsers.").
See also How do I conditionally apply CSS styles in AngularJS?

The angular-ui ui-if directive watches for changes to the if condition/expression. Mine doesn't. So, while my simple implementation will update the view correctly if the model changes such that it only affects the template output, it won't update the view correctly if the condition/expression answer changes.

E.g., if the value of a from.name changes in the model, the view will update. But if you delete $scope.data.messages[0].from, the from name will be removed from the view, but the template will not be removed from the view because the if-condition/expression is not being watched.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 1
    Thanks Mark, I am probably going to use UI-IF, for the reason you state, it's probably the most bullet-proof way of doing it. However, I looked at the code for ui-if and it's doing a hell of a lot of stuff I don't understand and I'm always nervous about using code I don't understand. – jonhobbs Dec 30 '12 at 02:28
  • I have just noticed though that ui-if doesn't replace itself, it still leaves a wrapper div around my template. presumably it has to do this for some reason, something to do with wanting to maintain the watch on the expression? – jonhobbs Dec 30 '12 at 03:05
21

You could use the ngSwitch directive:

  <div ng-switch on="selection" >
    <div ng-switch-when="settings">Settings Div</div>
    <span ng-switch-when="home">Home Span</span>
    <span ng-switch-default>default</span>
  </div>

If you don't want the DOM to be loaded with empty divs, you need to create your custom directive using $http to load the (sub)templates and $compile to inject it in the DOM when a certain condition has reached.

This is just an (untested) example. It can and should be optimized:

HTML:

<conditional-template ng-model="element" template-url1="path/to/partial1" template-url2="path/to/partial2"></div>

Directive:

app.directive('conditionalTemplate', function($http, $compile) {
   return {
      restrict: 'E',
      require: '^ngModel',
      link: function(sope, element, attrs, ctrl) {
        // get template with $http
        // check model via ctrl.$viewValue
        // compile with $compile
        // replace element with element.replaceWith()
      }
   };
});
asgoth
  • 35,552
  • 12
  • 89
  • 98
  • I have created a new fiddle using ng-switch as best i can - http://jsfiddle.net/jon64digital/pGwRu/5/ - However, I don't think it will work for a few reasons. (1) -when="" doesn't seem to work for checking if the attribute is missing. (2) Even if it did I believe the DOM would still be littered with empty divs and extra wrapper divs. – jonhobbs Dec 29 '12 at 15:29
  • I'm not advanced enough yet to write my own directive, but instinctively it feels wrong to use an ajax call to get the templates. Would it be possible to include the template in the HTML (inside the directive)? I'm guessing the expression would be an attribute on the directive tag (which could call a function). If the expression evaluated to true the inner template would be bound and shown, if the expression valuated to false then the element would be removed from the DOM. – jonhobbs Dec 29 '12 at 20:07
  • You can also use hardcoded templates (Strings), yes. but then it would be less reusable. – asgoth Dec 29 '12 at 20:08
  • I've just been pointed in the direction of Angular UI IF directive. It looks like what I need but I don't understand how it's working so I'm always cautious of blindly including something in my project when I don't understand what it's doing. – jonhobbs Dec 29 '12 at 20:09
  • Asgoth, I'm not talking about hardcoding the template in the JS code of the directive. I mean putting it between the outer tags of the directive e.g. Template goes here. – jonhobbs Dec 29 '12 at 20:42
  • Thanks for your help asgoth. If I could mark two answers as being right I would but I've ticked Mark's answer as the consensus seems to be that ui-if is the best way to go and he's explained to me a little about what it's doing. Thanks again though. – jonhobbs Dec 30 '12 at 03:07
  • Also: >>Be aware that the attribute values to match against cannot be expressions. They are interpreted as literal string values to match against. For example, ng-switch-when="someVal" will match against the string "someVal" not against the value of the expression $scope.someVal << (https://docs.angularjs.org/api/ng/directive/ngSwitch) – david.barkhuizen Oct 02 '14 at 12:15
6

You can use ng-show on every div element in the loop. Is this what you've wanted: http://jsfiddle.net/pGwRu/2/ ?

<div class="from" ng-show="message.from">From: {{message.from.name}}</div>
matko.kvesic
  • 223
  • 2
  • 7
  • 1
    I believe that ng-hide just sets display:none, it doesn't remove anything from the DOM. – jonhobbs Dec 29 '12 at 01:21
  • Also, in your example, "createdBy" still shows, even if "from" is being shown. It should only ever show "createdBy" if there is no from address. – jonhobbs Dec 29 '12 at 01:30
  • Yes, it doesn't remove elements from dom. For createdBy hide when from is there just add criteria to the condition: http://jsfiddle.net/pGwRu/3/ – matko.kvesic Dec 29 '12 at 01:42