Two table cells instead of one angularjs - angularjs

I'll try to explain my problem as simple as I can. I have something like this:
<table>
<tr>
<th ng-repeat="ordinate in ordinatesX">{{ordinate.ordinateLabel}}</th>
</tr>
</table>
This is just a small peace of my table but it is the problematic one. My ordinate object has an attribute abstract. When the abstract is set to true and DOM to make two th like this:
<th>{{ordinate.ordinateLabel}}</th><th></th>
So the first th is writing ordinate label and the second th needs to be empty.
But when the abstract is set to false I need only one th like this:
<th>{{ordinate.ordinateLabel}}</th>
I've tried something with the simplest custom directive like this:
.directive('ngTest', function() {
return {
restrict: 'A',
replace: true,
template: '<th></th><th></th>'
}
});
But even this wasn't working because of the problem with the replace: true and root element being tr.
Any ideas?

You can combine ng-repeat-start and ng-if
ng-if will not insert element in DOM.
<table>
<tr>
<th ng-repeat-start="ordinate in ordinatesX">{{ordinate.ordinateLabel}}</th><th ng-if="ordinate.abstract" ng-repeat-end></th>
</tr>
</table>

Related

How to use Angularjs to dynamically set spinner after table row bound

I'm updating the table via an ajax call, and want to display a spin.js spinner while the row.status == pending.
Basically I have a row fragment that successfully toggles from
<div class="spinner">
to
<div class="hide">
as the row computation progresses, (via ajax within a controller).
I'm happy with any mechanism that works!
What I'm struggling to do is, when the value is
<div class="spinner">
have a spinner showing, otherwise have it hidden.
<table class="table table-hover">
<thead>
<tr>
<th>url</th>
<th>status</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="row in result.results">
<td>{{row.link.url}}</td>
<td> <div loadingWidget class="{{(row.status == 'pending' ? 'spinner' : 'hide' ) }}"></div>{{row.status}}</td>
</tr>
</tbody>
</table>
My latest script - basically I don't really know what I'm doing,
app.directive('loadingWidget', function ($rootScope) {
return {
restrict: 'E',
scope: {
field: '=',
attributes: '=',
editMode: '='
},
link: function (scope, element, attrs) {
scope.spinit = function() {
$rootScope.$broadcast('spinit');
}
}
};
});
$scope.$on('spinit', function(element){
//react to event
new Spinner().spin(element);
});
You should use the ngHide directive. I'm having difficulty determining what you want your show/hide condition to be though. The rule is that if the contents of ng-hide evaluate to true, the element will be set to hidden.
If you absolutely need to switch the active class, not just hide the element, user ng-class. That will set the class based on a variable in scope.
<table class="table table-hover">
<thead>
<tr>
<th>url</th>
<th>status</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="row in result.results">
<td>{{row.link.url}}</td>
<td>
<div loadingWidget class="spinner" ng-hide="row.status"></div>
{{row.status}}
</td>
</tr>
</tbody>
</table>
Also, using $broadcast on root scope can be incredibly expensive, and is almost never necessary. In your case I don't see why you can't just call the Spinner().spin method directly.
ngHide docs: https://docs.angularjs.org/api/ng/directive/ngHide
ngClass docs: https://docs.angularjs.org/api/ng/directive/ngClass
Edit:
Okay, so I went through your code more thoroughly. You have several issues here.
First, you directives are kebab case in html, camelcase in javascript. That means your directive should look like: <div loading-widget class="spinner" ng-hide="row.status">.
Also, you have restrict E on your directive attributes. This means it can only be used as an element, and you are using it as an attribute. To fix this, either remove the restrict option or change it to restrict: 'A'.
You don't need the scope on your loadingWidget directive at all, and honestly any time you include Root Scope you are probably doing something wrong.
All of this results in a directive that looks something like this:
app.directive('loadingWidget', function() {
return {
restrict: 'A',
scope: {
field: '=',
attributes: '=',
editMode: '='
},
link: function (scope, element, attrs) {
console.log("elm: ", element[0]);
new Spinner().spin(element[0]);
}
};
});
Plunkr Link
Also, there are already wrapper modules for Spin.js in Angular, so if you don't mind more dependencies that is a good option (Link).
In terms of the logic for hiding the spinner, I would simply use an ngShow with the value of whatever will be in that cell. When you go to update the value, set it to '' or false or undefined. On page load or when it gets changed to one of the values I just mentioned, the spinner will be hidden. Once the value is set, it will no longer be falsy and the element will show. This works as long as you never expect to store false in the variable. In Angular, don't use events unless you really need to, just manipulate scope variables and wait for the digest cycle. Having some simple state variables and then a combination of ngShow and ngHide can make simple state changes in a view easy.
A core value of getting comfortable with writing good Angular code is knowing that you basically never interact with your application outside of scope or directives. Avoid element selectors, and avoid events unless you are sure you need them. Much of Angular was designed to avoid the JQuery like tools, because they can make your code really hard to read.

Nested ng-repeat on same element without flattening original data source

I am using ng-repeat to generate table rows for the following object
$scope.things = [{
name: 'Bob',
otherThings: [{
foo: 'hello'
},{
foo: 'bye'
}]
},{
name: 'Sid',
otherThings: [{
foo: 'cake'
},{
foo: 'fish'
}]
}];
The ng-repeat expression I have gone with is:
<tbody ng-repeat="thing in things">
<tr ng-repeat="otherThing in thing.otherThings">
<td>{{thing.name}}</td>
<td>{{otherThing.foo}}</td>
</tr>
</tbody>
This generates multiple tbody elements, one per parent item in my object:
<tbody>
<tr>
<td>Bob</td>
<td>hello</td>
</tr>
<tr>
<td>Bob</td>
<td>bye</td>
</tr>
</tbody>
<tbody>
<tr>
<td>Sid</td>
<td>cake</td>
</tr>
<tr>
<td>Sid</td>
<td>fish</td>
</tr>
</tbody>
I want to avoid multiple tbody elements, but I know in order to access both parent and child items, I will have to nest the ng-repeat's.
I could flatten the original object so that the parent information is duplicated for each child, but I want to avoid this if possible and use the data structure as is.
Are there any special uses of ng-repeat, e.g. multiple expressions that could be used on one ng-repeat to give access to both parent and child items in each iteration?
Thanks
UPDATE
The reason I want to avoid multiple tbody elements is because I have a css selector for alternate row shading:
tbody tr:nth-child(odd){
background-color: $theme-list-alt;
}
If each parent only had one child item they would all get coloured the same, whereas I want alternate row colours across throughout.
Update 2
I wondered whether using ng-repeat-start and ng-repeat-end could help me here, so I tried with the following Plunkr
This doesnt give me a row per child, e.g:
Bob hello
Bob bye
Sid cake
Sid fish
I can see why, just not sure how I can achieve this.
Try this.
<tbody>
<tr ng-repeat="thing in things">
<td>{{thing.name}}</td>
<td ng-repeat="otherThing in thing.otherThings">{{otherThing.foo}}</td>
</tr>
</tbody>

Angular UI-Router ui-sref ignore some elements

I have an interesting problem with the uiSref directive and I haven't been able to find a solution (well an elegant one anyway) anywhere on the web. Basically I have a requirement from a client to be able to click a row in a table of resources and go to the editing view for that resource. Normally the uiSref directive works beautifully, but the problem resides in the fact that I have a Bootstrap dropdown in the last <td> of the table with a bunch of quick actions in it. The HTML looks something like this:
<table class="table table-bordedered table-hover">
<thead>
<tr>
<td>Name</td>
<td>Actions</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="resource in resources" ui-sref="edit({id: resource.id})">
<td ng-bind="resource.name"></td>
<td class="actions-column">
<div class="btn btn-xs btn-default" data-toggle="dropdown">
<i class="fa fa-cog"></i>
</div>
<ul class="dropdown-menu pull-right">
<li>
SOMETHING CRAZY
</li>
</ul>
</td>
</tr>
</tbody>
</table>
The problem is that when I click on the button in the actions column, the uiSref overrides the default action of the dropdown and takes me to the edit page. Now you might be asking yourself "well that's easy, why can't you just stop the propagation of the event!?"... doesn't work. When I add this to the actions column:
<td class="actions-column" ng-click="$event.stopPropagation()">
It kills the functionality of the dropdown menu and nothing shows up. Right now I have a workaround in place where I define an ngClick on the <tr> element that then deciphers where the state should go depending on the element clicked like so:
<tr ng-repeat="resource in resources" ng-click="goToEdit(resource, $event)">
And The JS looks like this:
scope.goToEdit = function(resource, event) {
// if the event.target has parent '.actions-column' or is that column, do nothing else
// invoke $state.go('edit', {id: resource.id})
}
I hate it though and I have a lot of list views like this. All I'm looking for is an elegant and portable solution that hopefully works natively through UI Router like $event.stopPropagation() (Although I've poked through the UI Router source and can't seem to find a workable alternative). Basically I want to have my cake and eat it too. Anyway, it'll be interesting to see what the SO community can come up with or if what I'm asking for is not currently possible. Thanks!
I got it! While looking through the UI Router source some more, it appears that the click event will be ignored if the target attribute is populated on the element that the uiSref resides on. It may not be the most beautiful thing in the world, but it sure is easier than what I was doing before.
NOTE: This only works if you're using the whole jQuery library, not jQLite
So I wrote this directive:
app.directive('uiSrefIgnore', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.on('click', function(e) {
// Find the ui sref parent
var uiSref = elem.parents('[ui-sref]').first();
// Set the target attribute so that the click event is ignored
uiSref.attr({
target: 'true'
});
// Function to remove the target attribute pushed to the bottom
// of the event loop. This allows for a digest cycle to be run
// and the uiSref element will be evaluated while the attribute
// is populated
setTimeout(function() {
uiSref.attr({
target: null
});
}, 0);
});
}
};
});
That way, whenever I want to ignore the javascript event for just the uiSref directive, I can just add this to the html:
<tr ui-sref="edit">
<!-- any other elements -->
<td ui-sref-ignore>The element I care about</td>
</tr>
BOOM! Let me know what you guys think about the implications of this.

Get div items with angular

As in Jquery I use each function ($.each...)to get different elements from a div selection (in my case).
How can i get different row elements content from a table using angularjs?
Example :
<table id="myTable">
<tr><td>one text</td></tr>
<tr><td>two text</td></tr>
<tr><td>three text</td></tr>
</table>
Thanks.
The correct way to do things like this is to make a directive, this is pretty easy to do.
I'm sure you are used to jquery where you would do something like:
<table id="myTable">
<tr><td class="formatted">one text</td></tr>
<tr><td class="formatted">two text</td></tr>
<tr><td class="formatted">three text</td></tr>
</table>
And then do:
$('.formatted').each(function(){ ... format text here ...})
The directive is a very similar concept in angular. This would be the equivalent of the above:
<table id="myTable">
<tr><td formatted-text>one text</td></tr>
<tr><td formatted-text>two text</td></tr>
<tr><td formatted-text>class="formatted">three text</td></tr>
</table>
And then you define a directive:
myApp.directive("formattedText", function(){
return {
link: function(scope, element, attrs){
var text = element.text();
...do some formatting on text....
element.text(text);
}
}
});
In this case the link function will be called for every time angular encounters an element with a formatted-text attribute.
You can even match on other things such as class or element name, by adding a restrict: to your directive. Here is an example of the a few restrict value's and the equivalent jquery selector:
restrict: 'A' => $('[formatted-text]').each(link)
restrict: 'C' => $('.formatted-text').each(link)
restrict: 'E' => $('formatted-text').each(link)
So for example if I wanted to exactly match my initial jquery example above, you would do:
myApp.directive("formattedText", function(){
return {
restrict: 'C',
link: function(scope, element, attrs){
var text = element.text();
...do some formatting on text....
element.text(text);
}
}
});
Hopefully this makes sense and helps de-mistify directives a bit.
What do you want to do with the elements?
Using Angular, DOM manipulation should be reduced as much as possible and be strictly restricted to directives.
Here is some good reading stuff: How do i think in Angular if i have a jQuery background
To actually answer your question, one way to do it would be creating a directive, then access the DOM node inside the directives controller:
app.directive('row',function(){
return{
restrict: 'E',
controller: function($scope,$element,$attrs){
// $element is what you are searching for
}
}
});
If you are trying to vary the text styling according to whether it is in an odd or even row, how about using the ngRepeat and ngClass directives?
In controller:
$scope.items = ["one text", "two text", "three text"];
Template:
<table id="myTable">
<tr ng-repeat="item in items" ng-class="{odd:$odd}"><td>{{item}}</td></tr>
</table>
Stylesheet:
.odd {/* your styling */}

How to use ng-repeat without an html element

I need to use ng-repeat (in AngularJS) to list all of the elements in an array.
The complication is that each element of the array will transform to either one, two or three rows of a table.
I cannot create valid html, if ng-repeat is used on an element, as no type of repeating element is allowed between <tbody> and <tr>.
For example, if I used ng-repeat on <span>, I would get:
<table>
<tbody>
<span>
<tr>...</tr>
</span>
<span>
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
</span>
<span>
<tr>...</tr>
<tr>...</tr>
</span>
</tbody>
</table>
Which is invalid html.
But what I need to be generated is:
<table>
<tbody>
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
</tbody>
</table>
where the first row has been generated by the first array element, the next three by the second and the fifth and sixth by the last array element.
How can I use ng-repeat in such a way that the html element to which it is bound 'disappears' during rendering?
Or is there another solution to this?
Clarification: The generated structure should look like below. Each array element can generate between 1-3 rows of the table. The answer should ideally support 0-n rows per array element.
<table>
<tbody>
<!-- array element 0 -->
<tr>
<td>One row item</td>
</tr>
<!-- array element 1 -->
<tr>
<td>Three row item</td>
</tr>
<tr>
<td>Some product details</td>
</tr>
<tr>
<td>Customer ratings</td>
</tr>
<!-- array element 2 -->
<tr>
<td>Two row item</td>
</tr>
<tr>
<td>Full description</td>
</tr>
</tbody>
</table>
As of AngularJS 1.2 there's a directive called ng-repeat-start that does exactly what you ask for. See my answer in this question for a description of how to use it.
Update: If you are using Angular 1.2+, use ng-repeat-start. See #jmagnusson's answer.
Otherwise, how about putting the ng-repeat on tbody? (AFAIK, it is okay to have multiple <tbody>s in a single table.)
<tbody ng-repeat="row in array">
<tr ng-repeat="item in row">
<td>{{item}}</td>
</tr>
</tbody>
If you use ng > 1.2, here is an example of using ng-repeat-start/end without generating unnecessary tags:
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
angular.module('mApp', []);
</script>
</head>
<body ng-app="mApp">
<table border="1" width="100%">
<tr ng-if="0" ng-repeat-start="elem in [{k: 'A', v: ['a1','a2']}, {k: 'B', v: ['b1']}, {k: 'C', v: ['c1','c2','c3']}]"></tr>
<tr>
<td rowspan="{{elem.v.length}}">{{elem.k}}</td>
<td>{{elem.v[0]}}</td>
</tr>
<tr ng-repeat="v in elem.v" ng-if="!$first">
<td>{{v}}</td>
</tr>
<tr ng-if="0" ng-repeat-end></tr>
</table>
</body>
</html>
The important point: for tags used for ng-repeat-start and ng-repeat-end set ng-if="0", to let not be inserted in the page. In this way the inner content will be handled exactly as it is in knockoutjs (using commands in <!--...-->), and there will be no garbage.
You might want to flatten the data within your controller:
function MyCtrl ($scope) {
$scope.myData = [[1,2,3], [4,5,6], [7,8,9]];
$scope.flattened = function () {
var flat = [];
$scope.myData.forEach(function (item) {
flat.concat(item);
}
return flat;
}
}
And then in the HTML:
<table>
<tbody>
<tr ng-repeat="item in flattened()"><td>{{item}}</td></tr>
</tbody>
</table>
The above is correct but for a more general answer it is not enough. I needed to nest ng-repeat, but stay on the same html level, meaning write the elements in the same parent.
The tags array contain tag(s) that also have a tags array.
It is actually a tree.
[{ name:'name1', tags: [
{ name: 'name1_1', tags: []},
{ name: 'name1_2', tags: []}
]},
{ name:'name2', tags: [
{ name: 'name2_1', tags: []},
{ name: 'name2_2', tags: []}
]}
]
So here is what I eventually did.
<div ng-repeat-start="tag1 in tags" ng-if="false"></div>
{{tag1}},
<div ng-repeat-start="tag2 in tag1.tags" ng-if="false"></div>
{{tag2}},
<div ng-repeat-end ng-if="false"></div>
<div ng-repeat-end ng-if="false"></div>
Note the ng-if="false" that hides the start and end divs.
It should print
name1,name1_1,name1_2,name2,name2_1,name2_2,
I would like to just comment, but my reputation is still lacking. So i'm adding another solution which solves the problem as well. I would really like to refute the statement made by #bmoeskau that solving this problem requires a 'hacky at best' solution, and since this came up recently in a discussion even though this post is 2 years old, i'd like to add my own two cents:
As #btford has pointed out, you seem to be trying to turn a recursive structure into a list, so you should flatten that structure into a list first. His solution does that, but there is an opinion that calling the function inside the template is inelegant. if that is true (honestly, i dont know) wouldnt that just require executing the function in the controller rather than the directive?
either way, your html requires a list, so the scope that renders it should have that list to work with. you simply have to flatten the structure inside your controller. once you have a $scope.rows array, you can generate the table with a single, simple ng-repeat. No hacking, no inelegance, simply the way it was designed to work.
Angulars directives aren't lacking functionality. They simply force you to write valid html. A colleague of mine had a similar issue, citing #bmoeskau in support of criticism over angulars templating/rendering features. When looking at the exact problem, it turned out he simply wanted to generate an open-tag, then a close tag somewhere else, etc.. just like in the good old days when we would concat our html from strings.. right? no.
as for flattening the structure into a list, here's another solution:
// assume the following structure
var structure = [
{
name: 'item1', subitems: [
{
name: 'item2', subitems: [
],
}
],
}
];
var flattened = structure.reduce((function(prop,resultprop){
var f = function(p,c,i,a){
p.push(c[resultprop]);
if (c[prop] && c[prop].length > 0 )
p = c[prop].reduce(f,p);
return p;
}
return f;
})('subitems','name'),[]);
// flattened now is a list: ['item1', 'item2']
this will work for any tree-like structure that has sub items. If you want the whole item instead of a property, you can shorten the flattening function even more.
hope that helps.
for a solution that really works
html
<remove ng-repeat-start="itemGroup in Groups" ></remove>
html stuff in here including inner repeating loops if you want
<remove ng-repeat-end></remove>
add an angular.js directive
//remove directive
(function(){
var remove = function(){
return {
restrict: "E",
replace: true,
link: function(scope, element, attrs, controller){
element.replaceWith('<!--removed element-->');
}
};
};
var module = angular.module("app" );
module.directive('remove', [remove]);
}());
for a brief explanation,
ng-repeat binds itself to the <remove> element and loops as it should, and because we have used ng-repeat-start / ng-repeat-end it loops a block of html not just an element.
then the custom remove directive places the <remove> start and finish elements with <!--removed element-->
<table>
<tbody>
<tr><td>{{data[0].foo}}</td></tr>
<tr ng-repeat="d in data[1]"><td>{{d.bar}}</td></tr>
<tr ng-repeat="d in data[2]"><td>{{d.lol}}</td></tr>
</tbody>
</table>
I think that this is valid :)

Resources