Multi-level tables (inside another if clicked) - angularjs

Scenario
Lets say I am owner of a big company that has many stores. Depending on what role (place in the organization) I have within the company, I will have different access to data. There will be different modules and for this specific question there is one where users that have access can go through daily cost and sales.
(If that's legal or not...don't care, it's just for an example.)
The user thereby get all data through REST API from BackEnd (Java application) with all data for all stores the user has access to. The user should then be able to filter the data, by different filter combinations. Most relevant for my question is the date interval by days.
There will be some charts showing data on different levels and below there will be a table area where I want the multi-level tables, hence my question.
Done so far
I first created accordions that have stores on accordion-group level and then next level of data in a table, within the accordion-body. (Only hard coded data at the moment.) The problem here was that an according heading is a string and after some discussion we felt that this was not a good solution since the heading would consist of parts of data that in a table would have been separate columns. It would therefore be difficult to "columnize" the heading data to match horizontally the different "stores" (between the accordion headings) when collapsed (and of course even more messy when one or more accordion are expanded).
I replaced the accordions with table and ng-repeat. Have successfully populated the first table level with both data from the figurative API with JSON data as well as got i18next working for the headings.
JSON
{
"metadata":{
"storesInTotal":"25",
"storesInRepresentation":"2"
},
"storedata":[
{
"store" : {
"storeId" : "1000",
"storeName" : "Store 1",
"storePhone" : "+46 31 1234567",
"storeAddress": "Some street 1",
"storeCity" : "Gothenburg"
},
"data" : {
"startDate" : "2013-07-01",
"endDate" : "2013-07-02",
"costTotal" : "100000",
"salesTotal" : "150000",
"revenueTotal" : "50000",
"averageEmployees" : "3.5",
"averageEmployeesHours" : "26.5",
"dayData" : [
{
"date" : "2013-07-01",
"cost" : "25000",
"sales" : "15000",
"revenue" : "4000",
"employees" : "3",
"employeesHoursSum" : "24"
},
{
"date" : "2013-07-02",
"cost" : "25000",
"sales" : "16000",
"revenue" : "5000",
"employees" : "4",
"employeesHoursSum" : "29"
}
]
}
},
{
"store" : {
"storeId" : "2000",
"storeName" : "Store 2",
"storePhone" : "+46 8 9876543",
"storeAddress": "Big street 100",
"storeCity" : "Stockholm"
},
"data" : {
"startDate" : "2013-07-01",
"endDate" : "2013-07-02",
"costTotal" : "170000",
"salesTotal" : "250000",
"revenueTotal" : "80000",
"averageEmployees" : "4.5",
"averageEmployeesHours" : "35",
"dayData" : [
{
"date" : "2013-07-01",
"cost" : "85000",
"sales" : "120000",
"revenue" : "35000",
"employees" : "5",
"employeesHoursSum" : "38"
},
{
"date" : "2013-07-02",
"cost" : "85000",
"sales" : "130000",
"revenue" : "45000",
"employees" : "4",
"employeesHoursSum" : "32"
}
]
}
}
],
"_links":{
"self":{
"href":"/storedata/between/2013-07-01/2013-07-02"
}
}
}
Visual example - JSFiddle
Check the values in the result frame, top left corner. Try clicking for example row with Store ID 2000, then with 3000 and then 3000 again to see how the values change.
Current update of my JSFiddle
Wanted functionality
When a row is clicked (as shown in the JSFiddle), I want a directive or something triggered, to go and fetch underlying data (dayData) for the store clicked and show all days in the date interval. I.e expanding the row, and including a new table under the clicked row, which also should use ng-repeat to get all data displayed similar to the existing one, but inline.
Question
So I have already got the functionality to get the $index and also the specific data from the clicked row.
What kind of directive or any other solution do I need additionally to get the "data when row clicked" and presented in a table under the clicked row?
I don't want it in the DOM all the time, since there might be many dayData for each store and many stores. (And will use pagination later, but even so, not in the DOM all the time.)
This means that I have to be able to ADD when clicking a row, and when clicking the same or another REMOVE from the previously clicked.
EDIT
New updated JSFiddle.

The requirement was to not fill the DOM with all second level tables and I came up with a solution for it.
First I tried creating a custom directive and I got it to insert a row at the right place (beneath the clicked row) and created a second level table with headers but could not get it to populate with rows using ng-repeat.
Since I could not find a solution that worked fully I went back to what I already had created, with ng-show and sought a solution like it...and there exist one.
Instead of using ng-show/ng-hide, Angular has a directive called ng-switch.
In level one
<tbody data-ng-repeat="storedata in storeDataModel.storedata"
data-ng-switch on="dayDataCollapse[$index]">
...and then in the second level, i.e. the second tr
<tr data-ng-switch-when="true">
, in which you have the second level ng-repeat.
Here's a new JSFiddle.
Inspect elements, and you will see that there is a comment placeholder for each "collapsed" level 2 table.
UPDATE (2014-09-29)
Upon request of also expanding/collapsing level 3, here's a new JSFIDDLE.
NOTE! - This solution have all information in the DOM at all times, i.e. not as the earlier solution, which only added a notation for where the information should be added upon request.
(I can't take credit of this one though, since my friend Axel forked mine and then added the functionality.)

I think that a better solution is to use the ng-repeat-start and ng-repeat-end directives on the tr elements rather than using the ng-repeat on the tbody (this case doesn't justify more than a single tbody).
Have a look here:
PLNKR
<tr ng-repeat-start="person in people">
<td>
<button ng-if="person.expanded" ng-click="person.expanded = false"></button>
<button ng-if="!person.expanded" ng-click="person.expanded = true"></button>
</td>
<td>{{person.name}}</td>
<td>{{person.gender}}</td>
</tr>
<tr ng-if="person.expanded" ng-repeat-end="">
<td colspan="3">{{person.details}}</td>
</tr>

I don't have enough reputation yet to comment, so I'm following up in an answer with an additional functionality issue I have come across. I used Pixic's structure successfully as I really like that the DOM is not polluted with the second level data/tables until they are viewed and it is all working perfectly.
Until I was asked to allow dynamic sorting of the top level table. No problem - I can sort the top level table. The problem is I have been unable to come up with a way to keep the second level table (ie hidden ) synced up and "moving" with the sort of the top level table. This is a display only issue. The underlying data is properly associated with the top level, but the display is messed up as it displays the original "indexed" row as the second level tables don't re-sort with the parent row.
The only thing I can come up with is dynamically inserting the second level html from the controller upon expanding a row, but wondering if anyone else had any other ideas. I tried ng-repeat-start and ng-repeat-end (on a hidden third tr tag to include all elements thinking that should keep all items in the tbody together, and on a hidden tbody), but that didn't work.
Edit:
As requested, I have started another question: Multi-level tables (inside another if clicked) and Dynamic Sorting of Top Level Table

Related

Hide Bootstrap Table Rows in AngularJS (1.6)

I have a long table and I'd like to show only the first three entries by default. I'd like to provide something like the table entitled "Additional Metadata" shown on this page with a 'see more' option. The 'see more' link, once clicked, will show the rest of the rows in the table, and an option will appear at the bottom of the table now that says "see less".
I tried playing around with ng-show/ng-hide but couldn't accomplish the aspect where the "see more" link would show at the bottom of the table once all rows are shown. Any help would be appreciated.
Assuming you are using "ng-repeat" to populate the table, you should select an additional "index" or "row no" field and then use a "filter" to filter where the index is less than 4. clicking "see more" can then just change the filter.
Just use the limitTo filter to show n amount of first entries, when n can a scope variable (this is much the same way pagination is usually done in angular)
$filter('limitTo')(input, limit, begin)
You'll set limit to 3, and begin from 0
Inside your ngRepeat
ng-repeat="row in data | limitTo: showAmount : 0"
And inside your controller
$scope.showAmount = 3;

AngularJS Datatables with display length but without pagination

I'm working with angular-datatables.
My end goal is to have a table with no pagination but with a "link" that allow to "show 25 more entries" (and without using the select length "10", "25", "50","100" etc).
So first of all, I set the display length to 25 with :
this.dtOptions = DTOptionsBuilder.newOptions()
.withBootstrap()
.withDisplayLength(25)
Great my table is showing only 25 five entries with pagination.
No I try to disable the pagination with :
this.dtOptions = DTOptionsBuilder.newOptions()
.withBootstrap()
.withDisplayLength(25)
.withOption('paging', false)
And then the pagination is disable but the table is showing more than 25 entries.
I get that it's kind of logic since otherwise there will be no means to acces those 25+ entries but that's what i'd liked to acheive.
Thanks
I would simply hide the injected pagination element when the table is finished rendering :
.withOption('drawCallback', function() {
$('.dataTables_paginate').hide()
})
.dataTables_paginate is the overall <div> where dataTables places the pagination control, if any.

Initializing ngModel with data - AngularJS Bootstrap bs-select

I am maintaining a site that allows users to create a profile of sorts that will allow them to broadcast activities to a feed. I implement ng-grid to keep track of all the profiles that are created, and have created two buttons that allow users to create/edit these profiles. My only problem right now is, when users select a row on the grid and attempt to edit that specific row, the drop-down menu is not auto-populated with the data from ngModel.
This is the part of the form I am having trouble with:
<select ng-model="source.canSendTo" ng-options="value.name for value in sourceCanSendTo" data-style="btn" bs-select></select>
And within the controller, I have sourceCanSendTo defined as:
$scope.sourceCanSendTo = [ {"id":"abc", "name": "ABC"}, {"id":bcd", "name": "BCD"} ... ];
On row selection, I simply set source = the selected item, and console.logs show that all the data is there. The other parts of the form are being populated properly (mainly s), and console.log($scope.source.canSendTo) shows that the original data is there, it's just that select is defaulted to being blank...how would I go about trying to pre-select certain elements on the drop-down select I currently have?
For example, if the profile has 'abc', 'bcd' selected, how can I make it so that when I edit that profile, the drop down box shows 'abc,bcd' instead of just "Nothing Selected"?
Edit: I previously responded to a comment inquiring about bs-select, saying that it simply controlled some CSS elements of the drop down box - seems like this is completely incorrect after a quick google search when everything else led to dead ends. Does anyone have any idea how to properly initialize the model with data so that when I preload my form, the 'can send to' drop down select actually has the selected options selected, as opposed to saying "Nothing Selected"? Thanks in advance for all help!
As you are binding source.canSendTo to the name (value.name) of sourceCanSendTo then you just need to initially have an structure binding the names which had been saved, something like this:
source.canSendTo = ['abc', 'bcd']; //And all the selected values
So you need to construct your source.canSendTo property to this structure.
PS: If you show how you bring your data from the server, I can help you to construct the source.canSendTo property.
$scope.canSendTo must be initialized with a reference to the selected option.
var initialSelection = 0;
$scope.source = { canSendTo : [ {"id":"abc", "name": "ABC"}, {"id":bcd", "name": "BCD"} ... ] };
$scope.canSendTo = $scope.source.canSendTo[initialSelection];
Finally found out what was wrong with my code - seems like the data being stored in the model wasn't the same as what was in ngOptions, played around a bit with ngOptions and managed to get something that works. Working snippet of code:
<select ng-model="sendTo.name" ng-option="value.name as value.name for value in sourceCanSendTo" data-style="btn" multiple bs-select>
(Realized that the variable being used for ngModel was a fairly ambiguous in terms of naming convention, changed it)

How do you update ng-grid gridOptions.sortInfo?

I am trying to programmatically update the gridOptions.sortInfo on the Angular grid ng-grid, but I can't get it working.
I have both "name" and "age" columns. I am initially setting the sort on the "name" column but would like load some new data and then update it to sort on "age". (Programmatically, not just by clicking the column header).
I can set the $scope.gridOptions.sortInfo to a new value, but the grid does not reflect this. What is the correct way to update the gridOptions.sortInfo ?
Please see plunker:
http://plnkr.co/edit/JBYnrwLAIwFSKS6uSAND?p=preview
EDIT: Please note I would like to be able to update the sort direction i.e. ascending/descending as well as the actual column to sort on.
Many thanks
Yes, according to source code (line number 128 here link) you should be able to just do like this:
$scope.updateSortInfo = function() {
$scope.gridOptions.sortBy('name');
}
The plunker shows that it works. I forked it here.
Here's another version with sortColumn passed in from a text box: plunker.
Turns out if you call $scope.gridOptions.sortBy() twice on the same column, it will sort in descending order.
Look at this: http://ui-grid.info/docs/#/api/ui.grid.class:GridOptions.columnDef
Search for this text:
sort
An object of sort information, attributes are:
I needed to sort by the id column at the beginning in desc order and that did the trick for me:
var columnsDef = [
{name:'id',width:50,sort: {
direction: 'desc',
priority: 0
}},
{name:'title',width:350},
{name:'address'},
{name:'date',width:150},
{name:'description',minWidth:450}
];
$scope.theGrid.columnDefs = columnsDef;

Anything like template references in AngularJS?

I'm trying to create a form whose layout is entirely data driven.
Example data source:
{
title : "Form Test",
fields : [{
name : "FieldA",
type : "string",
value : "initial value"
}, {
name : "FieldB",
type : "selection",
options : ["1", "2", "3"],
value : "2"
}, {
name : "FieldC",
type : "struct",
value :
[{
name : "FieldC1",
type : "string",
value : "initial value"
}, {
name : "FieldC2",
type : "string",
value : "initial value"
}
]
}
]
}
I think can use ng-repeat and ng-switch to choose the form element depending on the 'type', however I get stuck when it comes to doing this recursively when I get to 'FieldC'.
<span ng-switch on="field.type">
<div ng-switch-when="string">STRING: {{field.value}}</div>
<div ng-switch-when="selection">SELECTION: {{field.value}}</div>
<div ng-switch-when="struct">STRUCT: ????</div>
<div ng-switch-default>DEFAULT:{{field.value}}</div>
</span>
Essentially I want a way that when I encounter a "struct" it recursively applies the ng-switch to the struct fields? Is there any way to "reference" the template so it can be used in multiple places on the same page? The support for template "partials" seems to need to be coordinated server-side via routes which seems like overkill here. Is this something where I need to start digging into creating my own directives?
EDIT I just stumbled across this that looks like it has a decent chance of doing what I want (I have yet to properly test it), is that in the right direction?
You'll want to build a directive that takes this kind of data and builds the form from it.
The way to treat the recursion is to treat every level, including the top level, as another struct. I built a version here: http://jsfiddle.net/U5Kyp/9/
Make sure you read the directive guide in the docs so you understand what's happening: http://docs.angularjs.org/guide/directive
Here is an update of accepted answer for angular.js 1.0.1 There were a few non-compatible changes in stable version:
ng-app is now required directive
scope syntax and semantics were changed
http://jsfiddle.net/9qAfM/1/
In my opinion this is a bad case of the inner platform effect. Quoting wikipedia: "tendency of software architects to create a system so customizable as to become a replica, and often a poor replica, of the software development platform they are using".
AngularJS already has a powerful mechanism for traversing a tree of objects and building a stack of scopes and controllers out of it. You could argue that it is exactly what AngularJS IS.
If you are forced to build forms out of such abominable JSON, I think the easiest way is to turn them into HTML (by means of a simple template language of any kind, server side or client side) and then using the $compile service to turn them into an angularjs application.

Resources