I'm trying to create a recursive template in AngularJS. I've read quite some blog posts and SO posts about it, but cannot get my code to work.
I'm trying to make a recursive template to render simple nested expressions with variables and operators. In this case, the nodes in the data tree can be one of multiple types and each type is rendered differently. I try to address this with one Angular template, ng-switch and ng-include.
The template:
<script type="text/ng-template" id="expression.tpl">
<ng-switch on="expression.type">
<span ng-switch-when="two-sided-operator">
<ng-include src="'expression.tpl'" ng-init="expression = expression.left"></ng-include>
{{ expression.operator }}
<ng-include src="'expression.tpl'" ng-init="expression = expression.right"></ng-include>
</span>
<span ng-switch-when="variable"> {{ expression.name }} </span>
<span ng-switch-when="literal"> "{{ expression.value }}"</span>
</ng-switch>
</script>
<ng-include src="'expression.tpl'" onload="expression=testExpression"></ng-include>
The data:
$scope.testExpression = {
type: "two-sided-operator",
left: {
type: "variable",
name: "foo"
},
operator: "==",
right: {
type: "literal",
value: "bar"
}
};
This however does not render anything. If I remove the internal ng-includes, it does render at least expression.operator.
Can anyone tell me what I am doing wrong? Could there be an issue with the assignment to expression in the ng-init statement?
Thanks for reading this far!
Finally got this to work by using a hack of ng-repeat. The working template:
<script type="text/ng-template" id="expression.tpl">
<ng-switch on="expression.type">
<span ng-switch-when="two-sided-operator">
<span ng-repeat="expression in [expression.left]" ng-include="'expression.tpl'"></span>
{{ expression.operator }}
<span ng-repeat="expression in [expression.right]" ng-include="'expression.tpl'"></span>
</span>
<span ng-switch-when="variable"> {{ expression.name }} </span>
<span ng-switch-when="literal"> "{{ expression.value }}"</span>
</ng-switch>
</script>
I don't understand ng-include deep enough to explain why it did not work though :)
Related
I'm new in angular and I'm trying to access some datas inside a variable in which there is tab nested in another tab. (I can't link it because it's sensitive). Anyway, I'm trying to use a nested ng-repeat to access it, but the second ng-repeat is never called, and my console doesn't give me a single error message. This is basically how it looks.
dico = [
{ ...
content : []
},
{ ...
content : []
},
...
];
After checking existing questions (https://stackoverflow.com/questions/19206760/angular-nested-ng-repeat-failure#= & passing 2 $index values within nested ng-repeat) I don't get why my part isn't working.
<span ng-repeat="toolbar in dico">
<candidate-toolbar-review average="toolbar.average" labeltoolbar="{{toolbar.name}}">
<span ng-repeat="field in toolbar.content" layout="row" layout-wrap layout-align="start center">
<candidate-field-review labelfield="{{field.name}}" ng-model="field.value" my-model="field.checked">
</candidate-field-review>
</span>
</candidate-toolbar-review>
</span>
In this sample of code, both candidate-field-review and candidate-toolbar-review are directives, if I comment the first one, the second one will output correctly. Elsewise, the first one is printed as expected.
I tried to use $parent and track by $index, but I don't really understand how those work. I also tried to use div instead of span or to not use span at all (and include the ng-repeats inside the candidate-toolbar and candidate-field). What am I missing here ? Thanks !
EDIT :
I used those lines instead and it worked out. I still don't really know why it wouldn't work the other way around though.
<span ng-repeat="toolbar in dico">
<candidate-toolbar-review ng-model="toolbar.average" labeltoolbar="{{toolbar.name}}" ng-click="showSmart=!showSmart">
</candidate-toolbar-review>
<span ng-repeat="field in toolbar.content">
<candidate-field-review labelfield="{{field.name}}" ng-model="field.value" my-model="field.checked">
</candidate-field-review>
</span>
</span>
I believe that
<span ng-repeat="field in toolbar.content" layout="row" layout-wrap layout-align="start center">
is never called because toolbar exists only in <span ng-repeat="toolbar in dico"> scope.
candidate-toolbar-review has it's own controller.
to pass toolbar to candidate-toolbar-review you need todo something like:
.directive('candidateToolbarReview', function() {
return {
restrict: 'E',
transclude: true,
scope: {
toolbar : '#'
},
and:
<candidate-toolbar-review toolbar="toolbar" average="toolbar.average" labeltoolbar="{{toolbar.name}}">
<span ng-repeat="field in toolbar.content" layout="row" layout-wrap layout-align="start center">
I have two columns both containing items from the same array. I want to achieve the masonry effect since the height of each .tile will be different.
<div class="col-md-6">
<div class="tile" ng-repeat="item in items| orderBy: 'id'" ng-if="$odd">
<button ng-click="alert($index)"></button>
</div>
</div>
<div class="col-md-6">
<div class="tile" ng-repeat="item in items| orderBy: 'id'" ng-if="$even">
<button ng-click="alert($index)"></button>
</div>
</div>
Will Angular do the odd-even alternation on the sorted array or the way elements are stored in the array?
Angular is going to alternate on the way the elements are stored in the array. If you want to organize the array I would use .sort() before assigning to Angular $scope.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
Based on the trying the code below, it seems that Angular does apply $odd and $even after sorting based on the orderBy filter. However, this would only work correctly in a single ng-repeat directive. I would modify your code to look like the following:
angular.module("myApp", [])
.controller("ItemController", function($scope) {
$scope.items = [{
id: 50
}, {
id: 1
}, {
id: 2
}, {
id: 5
}, {
id: 3
}];
$scope.alert = function(idx) {
console.log(idx);
};
});
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="myApp">
<div class="col-md-6" ng-controller="ItemController">
<div class="tile" ng-repeat="item in items| orderBy: 'id'">
<button class="btn btn-danger" ng-if="$odd" ng-click="alert($index)">{{item.id}}</button>
<button class="btn btn-success" ng-if="$even" ng-click="alert($index)">{{item.id}}</button>
</div>
</div>
</body>
In other words, you should apply the ng-if directive to the element inside your ng-repeat directive. Depending on what exactly you want to alternate, it might be simpler to use ng-class or another directive that allows you to specify conditional attributes.
The Angular orderBy filter returns a copy of the source array. The ng-repeat is applied to the copy.
Your example case will work, but it will create twice as many $watch elements, and you might experience odd flickering of the data shifting back and forth between columns if the number of rows changes. Also, because arrays are 0 based, your rows will be backwards.
I'm trying to get Angular to generate a CSS slider based on my data. I know that the data is there and am able to generate it for the buttons, but the code won't populate the ng-switch-when for some reason. When I inspect the code, I see this twice (which I know to be correct as I only have two items):
<div ng-repeat="assignment in assignments" ng-animate="'animate'" class="ng-scope">
<!-- ngSwitchWhen: {{assignment.id}} -->
</div>
My actual code:
<div ng-init="thisAssignment='one'">
<div class="btn-group assignments" style="display: block; text-align: center; margin-bottom: 10px">
<span ng-repeat="assignment in assignments">
<button ng-click="thisAssignment = '{{assignment.id}}'" class="btn btn-primary">{{assignment.num}}</button>
</span>
</div>
<div class="well" style="height: 170px;">
<div ng-switch="thisAssignment">
<div class="assignments">
<div ng-repeat="assignment in assignments" ng-animate="'animate'">
<div ng-switch-when='{{assignment.id}}' class="my-switch-animation">
<h2>{{assignment.name}}</h2>
<p>{{assignment.text}}</p>
</div>
</div>
</div>
</div>
</div>
EDIT: This is what I'm trying to emulate, though with dynamic data. http://plnkr.co/edit/WUCyCN68tDR1YzNnCWyS?p=preview
From the docs —
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.
So in other words, ng-switch is for hardcoding conditions in your templates.
You would use it like so:
<div class="assignments">
<div ng-repeat="assignment in assignments" ng-animate="'animate'">
<div ng-switch="assignment.id">
<div ng-switch-when='1' class="my-switch-animation">
<h2>{{assignment.name}}</h2>
<p>{{assignment.text}}</p>
</div>
</div>
</div>
Now this might not fit your use case exactly, so it's possible you'll have to rethink your strategy.
Ng-If is probably what you need — also, you need to be aware of "isolated" scopes. Basically when you use certain directives, like ng-repeat, you create new scopes which are isolated from their parents. So if you change thisAssignmentinside a repeater, you're actually changing the variable inside that specific repeat block and not the whole controller.
Here's a demo of what you're going for.
Notice I assign the selected property to the things array (it's just an object).
Update 12/12/14: Adding a new block of code to clarify the use of ng-switch. The code example above should be considered what not to do.
As I mentioned in my comment. Switch should be thought about exactly like a JavaScript switch. It's for hardcoded switching logic. So for instance in my example posts, there are only going to be a few types of posts. You should know a head of time the types of values you are going to be switching on.
<div ng-repeat="post in posts">
<div ng-switch on="post.type">
<!-- post.type === 'image' -->
<div ng-switch-when="image" class="post post-image">
<img ng-src="{{ post.image }} />
<div ng-bind="post.content"></div>
</div>
<!-- post.type === 'video' -->
<div ng-switch-when="video" class="post post-video">
<video ng-src="{{ post.video }} />
<div ng-bind="post.content"></div>
</div>
<!-- when above doesn't match -->
<div ng-switch-default class="post">
<div ng-bind="post.content"></div>
</div>
</div>
</div>
You could implement this same functionality with ng-if, it's your job to decide what makes sense within your application. In this case the latter is much more succinct, but also more complicated, and you could see it getting much more hairy if the template were any more complex. Basic distinction is ng-switch is declarative, ng-if is imperative.
<div ng-repeat="post in posts">
<div class="post" ng-class="{
'post-image': post.type === 'image',
'post-video': post.type === 'video'">
<video ng-if="post.type === 'video'" ng-src="post.video" />
<img ng-if="post.type === 'image'" ng-src="post.image" />
<div ng-bind="post.content" />
</div>
</div>
Jon is definitely right on. Angular does not support dynamic ngSwitchWhen values. But I wanted it to. I found it actually exceptionally simple to use my own directive in place of ngSwitchWhen. Not only does it support dynamic values but it supports multiple values for each statement (similar to JS switch fall-throughs).
One caveat, it only evaluates the expression once upon compile time, so you must return the correct value immediately. For my purposes this was fine as I was wanting to use constants defined elsewhere in the application. It could probably be modified to dynamically re-evaluate the expressions but that would require more testing with ngSwitch.
I am use angular 1.3.15 but I ran a quick test with angular 1.4.7 and it worked fine there as well.
Plunker Demo
The Code
module.directive('jjSwitchWhen', function() {
// Exact same definition as ngSwitchWhen except for the link fn
return {
// Same as ngSwitchWhen
priority: 1200,
transclude: 'element',
require: '^ngSwitch',
link: function(scope, element, attrs, ctrl, $transclude) {
var caseStms = scope.$eval(attrs.jjSwitchWhen);
caseStms = angular.isArray(caseStms) ? caseStms : [caseStms];
angular.forEach(caseStms, function(caseStm) {
caseStm = '!' + caseStm;
ctrl.cases[caseStm] = ctrl.cases[caseStm] || [];
ctrl.cases[caseStm].push({ transclude: $transclude, element: element });
});
}
};
});
Usage
Controller
$scope.types = {
audio: '.mp3',
video: ['.mp4', '.gif'],
image: ['.jpg', '.png', '.gif'] // Can have multiple matching cases (.gif)
};
Template
<div ng-switch="mediaType">
<div jj-switch-when="types.audio">Audio</div>
<div jj-switch-when="types.video">Video</div>
<div jj-switch-when="types.image">Image</div>
<!-- Even works with ngSwitchWhen -->
<div ng-switch-when=".docx">Document</div>
<div ng-switch-default>Invalid Type</div>
<div>
I'm trying to define a directive in Angular which takes a DOM like this:
<example-directive href="{{ foo }}"><img src="{{ bar }}"></example-directive>
... and transforms it to ...
<img src="BAR_VALUE">
... but only if the href is defined. If {{ foo }} is empty, it should transform to
<img src="BAR_VALUE">
It also needs to respond appropriately when the value of {{ foo }} and {{ bar }} changes. I can't figure out the documentation sufficiently to manage it. How do you do it?
Since the conditional show/hide doesn't work for you, what you need is variable templates within the same directive: https://coderwall.com/p/mgtrkg
This way you will have one directive and you will select between two templates depending on the existence or not of the href.
I dont see a need to use a directive here. Just use ngIf:
<a ng-if="foo != ''" href="{{foo}}"><img src="{{ bar }}"></a>
<img ng-if="foo == ''" src="{{ bar }}">
ngIf is a newer directive. Check for the supporting versions.
UPDATE: Ok I misunderstood the problem. See if i got it right:
I would have two css classes:
.vis { display: normal; }
.hid { display : none; }
Then your directive could have a html like this
<span ng-show="foo != ''"><img src="{{ bar }}" /></span>
<span ng-show="foo == ''"><img src="{{ bar }}" /></span>
It's a bit of a hacky workaround because of the nature of your html (deleting only a wrapping element makes it tricky), but it should work.
I am creating a list using ng-repeat something like this
<div ng-repeat="file in files">
{{file.name}}
</div>
But for the last element alone I would like to have a class (<div class="last">test</div>) included to it. how can i achieve this using ng-repeat?
You can use $last variable within ng-repeat directive. Take a look at doc.
You can do it like this:
<div ng-repeat="file in files" ng-class="computeCssClass($last)">
{{file.name}}
</div>
Where computeCssClass is function of controller which takes sole argument and returns 'last' or null.
Or
<div ng-repeat="file in files" ng-class="{'last':$last}">
{{file.name}}
</div>
It's easier and cleaner to do it with CSS.
HTML:
<div ng-repeat="file in files" class="file">
{{ file.name }}
</div>
CSS:
.file:last-of-type {
color: #800;
}
The :last-of-type selector is currently supported by 98% of browsers
To elaborate on Paul's answer, this is the controller logic that coincides with the template code.
// HTML
<div class="row" ng-repeat="thing in things">
<div class="well" ng-class="isLast($last)">
<p>Data-driven {{thing.name}}</p>
</div>
</div>
// CSS
.last { /* Desired Styles */}
// Controller
$scope.isLast = function(check) {
var cssClass = check ? 'last' : null;
return cssClass;
};
Its also worth noting that you really should avoid this solution if possible. By nature CSS can handle this, making a JS-based solution is unnecessary and non-performant. Unfortunately if you need to support IE8> this solution won't work for you (see MDN support docs).
CSS-Only Solution
// Using the above example syntax
.row:last-of-type { /* Desired Style */ }
<div ng-repeat="file in files" ng-class="!$last ? 'class-for-last' : 'other'">
{{file.name}}
</div>
That works for me! Good luck!
You could use limitTo filter with -1 for find the last element
Example :
<div ng-repeat="friend in friends | limitTo: -1">
{{friend.name}}
</div>
The answer given by Fabian Perez worked for me, with a little change
Edited html is here:
<div ng-repeat="file in files" ng-class="!$last ? 'other' : 'class-for-last'">
{{file.name}}
</div>