jsRender template for table containing variable-length elements - javascript

I have a simple View Model that has the traditional one-to-many relationship: Customers have Orders.
I'm creating a table such that each line has a Customer name with a line of Orders following it. It's all working great, but unfortunately few Customers have the same amount of orders, so building the table ends up looking like a sideways bar chart.
I know there is a standard solution to this, but I can't for the life of me figure it out. What I'm going for is to have every row contain the same amount of cells. I've considered putting dummy data into my model, but that just smells bad. I can get the maximum amount of orders possible, but I can't seem to figure out how to design a template that can use any of that information to create uniform tables. Here's what I have so far:
//#data is my ViewModel containing a few other things
<table id="ordersTable">
<!--header stuff-->
<tbody>
{{for #data.Customers tmpl='tmplRow' /}}
</tbody>
// OrderRow.tmpl.html
{{if #index % 2 == 0}}
<tr class="tableRowAlt1">
{{else}}
<tr class="tableRowAlt2">
{{/if}}
<td>{{>Name}}</td>
<td></td>
{{for Orders }}
<td>
{{>OrderId}}
</td>
{{/for}}
</tr>
EDIT: So I've decided to just force the numbe of columns, but now I'm getting some strange behavior on my new template:
// OrderRow.tmpl.html
{{if #index % 2 == 0}}
<tr class="tableRowAlt1">
{{else}}
<tr class="tableRowAlt2">
{{/if}}
<td>{{>Name}}</td>
<td>
{{>Order[0].Id}}
</td>
<td>
{{>Order[1].Id}}
</td>
<!--...-->
</tr>
It's rendering "Order.0 is null or not an object." I've verified that every row has data in it, but yet the template thinks it does not. I imagine this has something to do with the fact that I'm looking for array elements, but I'm not sure how to go about accessing the elements by index in a way that jsRender will accept.

This may not qualify as an answer but its too long as a comment.
I don't 100% follow what you want. I'm guessing that if you have 5 customers and one customer has 10 orders and the 4 other customers have 7 orders, then you want 3 empty td tags for the other 4 customers each.
The first thing you need to know is the length of the longest order list. Lets say its 10. You then need to pass that into the OrderRow template. Actually, you need the difference between the max and the number of orders of the current customer. Lets call that delta.
Now, after the {{for Orders}} loop, you have two choices. You can either add a single td with a span of delta or you can add delta number of empty td tags. You want to do either of these additions only if delta is greater than 0. Assuming ~max is passed in as the maximum number of orders than something like:
{{if (~max - Orders.lengh) > 0 }}
<td colspan='{{ ~max - Orders.length }}'></td>
{{/if}}
What I'm confused about is the end result is going to look the same. It will still have the orders as a sideways bar chart on the left hand side of the table.
You mentioned "a standard solution to this". Can you point to a rendered example somewhere?

Related

How do I display an array / object using ng-repeat

I have an array that looks like the following:
I want to display that using angularJS ng-repeat, showing both count and value for each.
This is currently the HTML and angular I have but nothing displays.
I also don't need to display the 0 index in the array, I want to start from 1 if possible.
<tr ng-class="" ng-repeat="x in vm.data | filter: searchArray">
<td>{{x.value}}</td>
<td>{{x.count}}</td>
</tr>
I went back to the basics and understood that an ng-repeat is just a loop in javascript, and what do you need to do if you want to access data in JavaScript if your array is multi-dimensional? well you do a nested for loop, and thats exactly what I'm doing in this case.
This is the updated html/angularjs that displays the data I need:
<tbody>
<tr ng-repeat="x in vm.data">
<td ng-repeat="y in x">{{y.value}} {{y.count}}</td>
</tr>
</tbody>
Thank you guys again!

Nested ng-repeat with dynamic input

I am trying to display a JSON response in a table using ng-repeat. The problem is that not all objects recieved are the same. All of them have a date, short message and long message. There are also ones with an additional value list, differing in length. This list should be diplayed underneath the long message within its own table or list. I use the alert.slice().reverse() because I want the newest entries to be on top. The new objects are inserted using .push({values}).
<tbody class="AlTbody" ng-repeat="alerts in alerts.slice().reverse()" ng-class="className">
<tr class="Altr Aldate">
<td ng-show="{{alerts.Date}}"><b>{{alerts.Date}}:</b>
</td>
</tr>
<tr class="Altr Alshort " ng-click="toggleDetail($index)">
<td>{{alerts.S}}</td>
</tr>
<tr class="Altr " ng-show="activePosition == $index">
<td class="msgL">{{alerts.L}}
<!-- 1) <p ng-show="{{item.List}}"><br><ul><li>Previous values:</li> <li ng-repeat="vals in ValueList">{{vals.value}}</li></ul> </p>-->
<!-- 2) <p ng-show="{{List.txt}}"> <br><ul><li>Previous values:</li> <li ng-repeat="List in alerts.List">{{List.txt}}</li></ul> </p>-->
</td>
</tr>
</tbody>
I already tried two approaches as seen in the code. The first one displayed the list correct however it was displayed underneath every long message instead of only the one it belongs to. I used a new variable.
var l=valList.length;
scope.List=true;
while(l>-1){
scope.ValueList.push({value: valList[l]});
l--;
}
The second approach did not work at all because I could not find an index.
var l=valList.length;
var indexV= jQuery.inArray(currdate,scope.alerts.Date);
while(l>-1){
scope.alerts[indexV].List.push({txt: valList[l]});
l--;
}
edit:
This is the current output. There you can see two objects( date, short message and long message) and both of them have the previous values section. However only the upper object is supposed to diplay the list of previous values.
What you are trying to achieve is entirely possible, my advice is to start at a known point and work from there.
I have put together a jsfiddle to show you how nested ng-repeats will work. try and work from that point. As a side note it looks like your JSON structure is overly complex, if you can simplify that down I would.
https://jsfiddle.net/roscorcoran/wyu7tgxm/
<div ng-app="App" ng-controller="Ctrl1">
<div ng-repeat="alert in alerts">
<a ng-if="alert.Date">{{alert.Date}}</a>
<p ng-repeat="val in alert.L.vals">
<a ng-if="val && val.value">{{val.value}}</a>
</p>
<p ng-repeat="item in alert.List">
<a ng-if="item && item.txt">{{item.txt}}</a>
</p>
</div>
</div>
You can try to add a ng-if statement inside the ng-repeat loop:
<p ng-if="{{values.list}}"><br><ul><li>Previous values:</li> <li ng-repeat="vals in values.list">{{vals.value}}</li></ul> </p>-->
Okay so this works for me now. It still always displays "Previous Values:" but the values only display when they actually belong to the message.
<ul ng-if="alerts.List.vals"><li>Previous values:</li> <li ng-repeat="val in alerts.List.vals" >{{val.value}}</li></ul>
This might not be the best and most elegant solution but it works.
if(valList){
scope.alerts.push({Date:currdate,S:msgSn,L:msgLn, List:{ vals:[{value:valList[0]},{value:valList[2]},{value:valList[4]},{value:valList[6]}] } });
}else{
scope.alerts.push({Date:currdate,S:msgSn,L:msgLn });
}
I only display the even indexes of the value array because the list of values was a string which I split and every uneven entry is "):" which I don't need to display.

two-level hierarchy repeater in angularJS, without any output for the first level

I would like to generate a table in angular, which is nested in a two-level hierarchy which would thus need two nested ng-repeat. An example data structure would be:
Product.Features
Each product would have a list of features, e.g:
iPhone
3G
ScreenSize
Fruit
Color
Size
Texture
I have a list of products, and would like to output as columns the list of features. Hence, the output would be:
<td>3G</td>
<td>ScreenSize</td>
<td>Color</td>
<td>Size</td>
<td>Texture</td>
The problem with this is that when using ng-repeat for the list of products, I cannot generate a blank element. Also, I cannot use ng-repeat on it's own. Some sample code could be (does not work):
<ng-repeat ng-repeat="product in products">
<td ng-repeat="feature in product.feature">
{{feature.Name}}
</td>
</ng-repeat>
Note that the above code is a sample and does not work!
Is this somehow possible? I saw some answers on SO mentioning to use ng-repeat-start but I still could not see how this is possible.
You could use underscore to flatten your list of products, and then you only need one ng-repeat:
In your controller:
$scope.features = _.flatten(_.map(products, function(product) {return product.features}));
$scope.features will be ['3G', 'ScreenSize', 'Color'...]
Then in your view:
<td ng-repeat="feature in features">
{{feature}}
</td>
There's an SO question about flattening an object here.

HandlebarsJS each only prints last item in array

This is a branch off from this question: Handlebars.JS (w/ Dashbars) parse error "expecting open_endblock got inverse" (There's also a codepen therein.) I posted this there because I thought the problems were related, maybe (since {{else}} can be used with if's or each's,) but that turned out not to be the case.
Specifically:
I'm also having the problem of my outermost {{#each}} looping in such a way that only the last record returned is being output through the template. Everything logs in {{log this}} after that opening {{#each}}, but it's not even hiding in the HTML output somewhere.
So my table SHOULD have 4 rows, and objects 0 through 3 log to the console, but only the fourth item, item 3, is added to the table. Inner each's work as expected, as do any and all other iterator functions; I'm not sure why it's JUST the outer one that's failing. The JSON from which my array of objects is created validates in every single linter I've used, as does my JavaScript. Atom is supposed to have a handlebars linter, but it doesn't seem to actually...umm...work.
I AM using Dashbars with this, (with both its lodash.js and moment.js dependencies,) but this bug has existed since before I started using that library. FWIW, jQuery 2.1.3 IS installed, and loaded before any other library, and all of that is in the <head> tag. (I don't see where it would matter, but just in case.) And since JavaScript is involved, yes, I'm in Chrome. The version of Handlebars is the latest, 3.0.1, using the full version because my use-case doesn't allow pre-compiling. Another NB is that all of this is embedded in a .cfm file (not my choice,) so everything does get run through the CFML interpreter first.
So...I'd managed to outsmart myself again. I've created a fork of my original code, but HTTPS policy in my browser is keeping it from working =-\
The "money" differences are as follows (can't use SO's <ul> because I can't embed code in a bullet-block)
*I'd been trying to jam my template into a <tr> element, to avoid having Handlebars process more lines of code than I thought was necessary:
<tr class="searchResults" id="searchResultsHTML"></tr>
<script id="result-template" type="text/x-handlebars-template">
<!--- To accommodate for that this would all begin and end with a <tr>, I tried this: --->
{{#unless #first}}
<tr>
{{/unless}}
<!--- The rest of the template --->
{{#unless #last}}
</tr>
{{/unless}}
</script>
*In good-programmer fashion, I'd forked my actual file several times, trying different things. (Mostly did this to avoid endless git resetting and branching; I know git's meant for this sort of thing, but I just wanted to be able to refresh old and new tabs at once. Anyway!) Took out those {{unless}} blocks and...apparently, I'd misunderstood what that does (since the official documentation is a bit...scant...on that point. There's no TRUE inverse of {{#if}}) For the sake of those who don't want to bother clicking to the codepen:
<div class="searchResults" id="searchResultsHTML">
<script id="result-template" type="text/x-handlebars-template">
<table id="resultTable">
<tr>
<th>Personal Information</th>
<th>Education</th>
</tr>
<!--- The log tag DOES output all records, even the ones that aren't showing in the HTML. --->
{{#each this}} {{log #index}} {{log this}}
<tr>
<td>{{#with basicInformation}}
<p>{{MASTER_CUSTOMER_ID}} ({{CUSTOMER_STATUS_CODE}})<br />
{{SEARCH_NAME}}<br />
(<span class="lightBlue">ADD:{{ADDOPER}}–{{{d-format 'MM/DD/YYYY' (d-date 'YYYY-MM-DD' ADDDATE)}}}</span>)</p>
<p>DOB:{{{d-format 'MM/DD/YYYY' (d-date 'YYYY-MM-DD' BIRTH_DATE)}}}<br />
{{/with}}</td>
<!--- More table columns; six total --->
<td>{{#each education}}
<p>{{INSTITUTION_NAME}} ({{{d-format 'YYYY' (d-date 'YYYY-MM-DD' BEGIN_DATE)}}}–{{{d-format 'YYYY' (d-date 'YYYY-MM-DD' END_DATE)}}})</p>
{{else}}
<p>No education records found</p>
{{/each}}
</td>
</tr>
{{else}}
<tr class="alert largerError" id="errorMessageRow">
<td class="empty" id="errorMessage" colspan="6">Either there has been an error, or your search did not return any records from any datasource.</td>
</tr>
{{/each}}
</table>
</script>
Yes, I stuck with ColdFusion comments; since those are ONLY for my reference, or the poor next developer, they don't need to go to the client and that way Handlebars isn't taking valuable time rendering them. (I'm already annoyed Handlebars is going to have to churn through my header row and a bunch of other text.) This tool searches our databases for customer numbers that MIGHT represent accidental duplicate accounts, so if you search for John Smith...oi!
So for anyone who Googled their way over here...I'll be glad if this helps even one person avoid spinning their wheels the way I did.

Adding additional table rows using Angular after using ng-repeat

New to angular, still working out the many kinks that I hit. I've got a question to pose to you all.
I currently have a table that is being populated on load with ng-repeat. The data itself is a bunch of sums of money. One particular entry, is a collection of those sums. I have an ng-onclick on it to run my function getLadders() when clicked. I would like it to basically pan out below wherever that element is in the chart, and create new rows of the data that is in that particular element.
A mini - drawing of the data it, as I can not paste the exact data.
ID Name Total
1 Single $20
2 Single $35
3 Combination $60***
4 Single $10
That is the current chart, with combination being the data with the many breakdowns within it. On click of that row, I want it to do something like this.
ID Name Total
1 Single $20
2 Single $35
3 Combination $60***
3.1 Single $20
3.2 Single $20
3.3 Single $20
4 Single $10
Basically, break out into everything inside that one piece. I have the function written with the extracted data, I have, right now, plain old jQuery to select that row, and append a tablerow after it, but it ends up attaching it to the end of the whole table instead of at that line.
Any help?
Also, for added benefits, I also have an icon on that row that I am looking to alter as well. It's a plus sign, that I want to be a minus sign when the data is being shown, and then back to a plus sign afterwards.
<tbody ng-controller="myCDsController">
<tr ng-repeat="holding in holdings" ng-if="holding.type !== 'Ladder'" data-type="{{holding.typeClass}}" data-id="{{holding.id}}">
<td>{{holding.type}}</td>
<td contenteditable="true">{{holding.name}}</td>
<td>{{holding.maturityDate}}</td>
<td>{{holding.amount | currency:"$"}}</td>
<td>{{holding.rate | percentage:2}}</td>
</tr>
<tr ng-repeat="holding in holdings" ng-if="holding.type == 'Ladder'" ng-controller="openLaddersController" data-type="{{holding.typeClass}}" data-id="{{holding.id}}">
<td><i ng-class="{'icon-dislike': !icon, 'icon-plus':icon}" ng-click="openLadders(holding.id)" >dfgh</i> {{holding.type}}</td>
<td contenteditable="true">{{holding.name}}</td>
<td>{{holding.maturityDate}}</td>
<td>{{holding.amount | currency:"$"}}</td>
<td>{{holding.rate | percentage:2}}</td>
</tr>
</tbody>
Here's some guidance based on what you have above. You should probably simplify the table structure into something like this:
<tbody ng-controller="myCDsController">
<tr ng-repeat="holding in holdings" data-type="{{holding.typeClass}}" data-id="{{holding.id}}">
<td><i ng-if="holding.type === 'Ladder'" ng-class="{'icon-dislike': !icon, 'icon-plus':icon}" ng-click="openLadders(holding)" >dfgh</i>{{holding.type}}</td>
<td contenteditable="true">{{holding.name}}</td>
<td>{{holding.maturityDate}}</td>
<td>{{holding.amount | currency:"$"}}</td>
<td>{{holding.rate | percentage:2}}</td>
</tr>
<tr ng-show="!icon" ng-if="holding.type === 'Ladder'>
<td colspan="5">
<table>
<tbody>
<tr ng-repeat="child in holding.children">
<td><!-- insert child content here --></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
There is probably a better way to handle the data-id / data-type attributes above, but I need more context.
Also, notice that I removed one of your controllers; I somehow doubt you need two controllers for this use case. Your controller should look something like this, probably:
app.controller('myCDsController', function(holding) {
$scope.openLadders = function() {
holding.children = getChildRows(holding)
}
})

Categories