Conditional logic in AngularJS template - javascript

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.

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:
1.1.5 also introduced a ternary operator
ng-switch can be used to conditionally add/remove elements from the DOM
see also How do I conditionally apply CSS styles in AngularJS?
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.

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()
}
};
});

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>

Related

Is it possible to Angularjs ng-switch does not re render view?

In some page of my site, i have some directives inside ng-switch, like this:
<div ng-switch="contentMenuObj.model">
<div ng-switch-when="calendar">
// Some directive
</div>
<div ng-switch-when="history">
// Some directive
</div>
</div>
Every time that I change the "view" (calendar to history) and go back (history to calendar) the angular re-render the calendar view and new queries are make in the server.
My question is about this behavior, is it possible to angular does not re-render the views? If impossible, what is the best way to solve this problem?
If I understand correctly. You would like to not rerender view when contentMenuObj.model change - right? If so apply one way binding
<div ng-switch="::contentMenuObj.model">
<div ng-switch-when="calendar">
// Some directive
</div>
<div ng-switch-when="history">
// Some directive
</div>
</div>
Model in that case will be loaded only once.
Or try to use ng-show / ng-hide directives if you would like to load directives only once.
<div ng-show="contentMenuObj.model == 'calendar'">
// Some directive
</div>
<div ng-show="contentMenuObj.model == 'history'">
// Some directive
</div>
Angular re-render your directives because ng-switch remove div when ng-switch-when condition is not satisfied, which makes directive is destroy and next time must be re-render.
The ng-show directive in contrast to ng-switch directive not remove element, but only hides.
So, if you want to hide and show content without re-rendering, try:
<div>
<div ng-show="contentMenuObj.model === 'calendar'">
// Some directive
</div>
<div ng-show="contentMenuObj.model === 'history'">
// Some directive
</div>
</div>

Passing value out of uib-accordion scope to the parent scope

I am using the uib-accordion directive from ui-boostrap for AngularJS.
I have a question about passing value out of the transcluded directive, uib-accordion-group.
When simply setting a variable to ng-model inside the accordion, it will be attached to the accordion scope, rather than the parent main scope, though it looks like it is in the main scope due to the transclude directive.
In order to pass the value inside the accordion out to the main scope, I need to do something like ng-model="$parent.$parent.$parent.data2 which seems wrong.
Is there a way to do it gracefully?
angular.module('ui.bootstrap.demo', ['ngAnimate', 'ui.bootstrap']);
angular.module('ui.bootstrap.demo').controller('AccordionDemoCtrl', function($scope) {
});
<!doctype html>
<html ng-app="ui.bootstrap.demo">
<head>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular-animate.js"></script>
<script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.14.3.js"></script>
<script src="example.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div ng-controller="AccordionDemoCtrl">
<script type="text/ng-template" id="group-template.html">
<div class="panel {{panelClass || 'panel-default'}}">
<div class="panel-heading">
<h4 class="panel-title" style="color:#fa39c3">
<a href tabindex="0" class="accordion-toggle" ng-click="toggleOpen()" uib-accordion-transclude="heading"><span
ng-class="{'text-muted': isDisabled}">{{heading}}</span></a>
</h4>
</div>
<div class="panel-collapse collapse" uib-collapse="!isOpen">
<div class="panel-body" style="text-align: right" ng-transclude></div>
</div>
</div>
</script>
<uib-accordion close-others="oneAtATime">
<uib-accordion-group heading="Static Header, initially expanded" is-open="true">
<div>
Simple data model
<input type="text" ng-model="data" />Anti-pattern data2 model
<input type="text" ng-model="$parent.$parent.$parent.data2" />
</div>
<div>
I read "{{data}}" inside the accordion
</div>
<div>
I read "{{data2}}" inside the accordion
</div>
</uib-accordion-group>
</uib-accordion>
<div>
How do I read "{{data}}" OUTSIDE the accordion
</div>
<div>
Data2 seems fine "{{data2}}" OUTSIDE the accordion
</div>
</div>
</body>
</html>
I had a related issue quite recently, and I ended up modifying the ui-bootstrap-tpls-0.14.3.js file. On line 239 you can see I've added a property called 'model' to the accordion directive's scope object.
scope: {
heading: '#', // Interpolate the heading attribute onto this scope
isOpen: '=?',
isDisabled: '=?',
model: '=' // Custom property added
},
Then in the controller, I've added an object called item1 to the controller's scope and given it a property called name:
$scope.item1 = {
name: 'test1'
};
Lastly, add a 'model' attribute to the accordion group directive and specify item1 as the value to pass into the accordion directive:
<uib-accordion-group model="item1" heading="Static Header, initially expanded" is-open="true">
You should now be able to set the object's property values in the accordion as well as access them outside of the directive.
Here's a plunk with your code modified to work.
http://plnkr.co/edit/44x8pH?p=preview
I'm not super happy with modifying the UI bootstrap file, but I couldn't come up with a better way to do it. Obviously you'll need to have your own copy of the file stored locally so you can modify it, and I'd suggest adding .custom to the filename to remind you that it's been modified. Probably pop in some comments too so you can migrate any changes you make to a future version of it in case you want to upgrade.
I know it may be very late, and a little bit of purpose, but I ran into a similar issue: I had a value to pass to the template. Inspired by Chris answer, I found a hack that does’nt change uib files:
I passed the value I want to ng-disable, which I used as a temporary variable.
<div uib-accordion-group ... is-disabled="color">
Then in my accordion template, I put isDisabled back to it’s default false value.
<div class="panel-heading" ng-init="color = isDisabled; isDisabled = false">
After this you can use the color angular variable anywhere in the template.
Here is the corresponding plunker : https://embed.plnkr.co/ExktoE4RCXFrn6SoYZIR/
note: It may also work to pass an object in which you have a value containing isDisabled wanted value ({foo: 'bar',.., isDisabled: true}). And then you pass this value back to isDisabled

In Angular, can I transclude content from a template url?

I'd would like to transclude content from a template url instead of from within the body of the directive tags.
Using a modal as an example, you could see the need to have the modal directive declaring a template url for the modal wrapper markup, however for the transcluded content instead of sourcing that content from within the directive tags (some modals may have extensive markup) I'd like to use a template url to provide the transluded content. Thus provided a much cleaner abstraction layer for the component.
i.e.
homeView.html
<my-modal transclude-content="profile"></my-modal>
modalDirective.js
app.directive('modal', function() {
return {
templateUrl: 'app/shared/modalView.html',
// some nifty way to transclude content from say 'app/shared/partials/' + attr.transcludeContent + '.html
};
});
modalView.html
<div class="modal">
<div class="modal-controls">
<span class="minimize">-</span>
<span class="close">x</span>
</div>
<div class="modal-body" ng-transclude>
</div>
</div>
The answer is simple!
Nest your directives!!
<modal-window>
<user-profile></user-profile>
</modal-window>

Angular ng-repeat, Jquery fail

So I am using ng-repeat to repeat some divs which show images out of my JSON file. What I want to do is that when I click on that image (whether its desktop or mobile) the image will scale. Now my problem is that when I want to create a click event on my image tag (which is inside that div that holds the ng-repeat), he doesn't do anything. He cant see the click.
I red something on the internet about issues with jquery and angular, but for me as a beginner its hard to understand what I have to do to make it work how I pleased. I just want to be able to put a jquery function on a image tag inside the ng-repeated divs, so I can manipulate the css from there.
I have a piece of the code posted below here, maybe I have to add something to my controller? I dont know, I am clueless at the moment. :-)
<section class="words">
<div class="colored-sidebar"></div>
<!-- content -->
<div class="previous-button"></div>
<div class="word-container" ng-controller="imageController as imageCtrl">
<h1><span>noun</span>words</h1>
<div class="category-body">
<p><span>noun</span>travel</p><hr>
<div class="category-section" ng-repeat="icon in imageCtrl.imageList.travel">
<!-- <div class="category-image" ng-include="icon.src"></div> -->
<div class="category-image">
<img src="{{icon.src}}" />
</div>
</div>
</div>
</section>
The angular file
(function() {
app.controller('imageController', function(){
this.imageList = imageJson;
});
var imageJson = {
//ALOT OF JSON DATA HERE//
};
})();
I hope this piece of code would be enough to help me :-)
Any tips are welcome, I love to learn this language better and also understand it better.
Thanks!
jQuery is not suitable here, because by the time you run your jQuery code inside jQuery.ready(), the elements in "category-image" class are not created yet.
For solution of your problem you can use two methods:
1) Use the "ng-click", as proposed before. You can also pass "$index" to function inside ng-click. This way you will know index of icon in imageList.travel that was clicked. But this way you will have no information about dom element.
2) Create a directive. The main difference between directives and controllers is that directive have information about dom object. You can treat element as typical jQuery object
JS:
app.directive('imageClick', [function () {
return {
link: function (scope, element, attr) {
element.on("click", function(e){
//do some stuff here
})
}
}
}]);
HTML
<section class="words">
<div class="colored-sidebar"></div>
<!-- content -->
<div class="previous-button"></div>
<div class="word-container" ng-controller="imageController as imageCtrl">
<h1><span>noun</span>words</h1>
<div class="category-body">
<p><span>noun</span>travel</p><hr>
<div class="category-section" ng-repeat="icon in imageCtrl.imageList.travel">
<!-- <div class="category-image" ng-include="icon.src"></div> -->
<div class="category-image">
<img image-click src="{{icon.src}}" />
</div>
</div>
</div>
</section>

How to expand DIVs the "angular way" (using angular masonry)

I'm trying to expand a DIV element on my angular layout. I'm using angular-masonry to give a mason-style to my layout, but now I need to expand those boxes on click. I've tried a lot of stuff, but it kept overlapping my others elements. Soon figured out that I'll have to write it the "angular way" so I don't run into DOM manipulation conflicts.
Here's my code:
<div class="row" masonry>
<div
class="masonry-brick item-component col-sm-4 col-md-4"
ng-repeat="component in components.components | filter : components.filterByFilter | filter : searchText"
ng-click=" // expand #expandable // "
>
<div class="component-wrapper">
<div class="component">
<img ng-src="#{{ component.thumb }}"/>
</div>
<div class="component">
#{{ component.name_en }}
</div>
</div>
<div id="expandable" class="expand-me codes-wrapper">
<p>XXX</p>
<p>YYY</p>
<p>ZZZ</p>
</div>
</div>
</div>
Here's what I want to accomplish in the "angular way": http://codepen.io/desandro/pen/daKBo
In your example (http://codepen.io/desandro/pen/daKBo) if you click on an element there are two things that will be done:
(1) the style of the clicked item is changed
(2) the function masonry is called on the container element that keeps the divs.
I can't see such a function in angular-masonry pre builded. So i'll guess you have to do this by your self. Here are some hints how to solve this (i havn't try it in real)
Bind a function to ng-click. In this function set a state to the current component. This state shoud be used to toggle the css-class of the element. you can use ng-class for this.
The second part is little bit more complex. I would suggest write a direcive 'masonry-change-listener' and bind it to the element that is bound to the same element with the directive masonry. If you click on a component $emit an event, that something has changed. In the directive 'masonry-change-listener' listen to this event. if this event fires you have to call $element.masonry.apply($element) in the link function.

Categories