Granular rendering control inside a knockout.js `foreach` control binding - javascript

I am new to knockout, and I can't seem to find this problem stated or documented elsewhere, though I suspect I might be missing something.
I have a foreach in my knockout template HTML as such:
...
<tbody data-bind="foreach: cfeeds">
<tr class="datarow" data-bind="css:{danger:discovered()==false}">
<td><a title="View this feed" data-bind="text: name,attr: {href: view_url}"></a></td>
<td data-bind="text: description, css:{'text-danger':discovered()==false}"></td>
<td class="text-center" data-bind="text: sched_type"></td>
<td class="text-center" data-bind="text: sched_data"></td>
<td class="text-center" data-bind="text: moment(last_poll()).calendar()"></td>
<td class="text-right">
<div class="feedgraph">
<!-- D3 chart container -->
</div>
</td>
</tr>
</tbody>
...
In the last DIV with class="feedgraph", I add (in this case) a D3 chart, which works fine when the DIV is outside the foreach. However, it appears that knockout will rerender the entire TR every time the JSON poll returns, whether the JSON contains a changed value or not, and my .feedgraph div gets emptied.
Relevant viewmodel blocks:
...
self.cfeeds = ko.observableArray([]);
...
self.loadFeeds = function() {
postJSON(ajax_uri+'get_site_feeds/',{'site_id':selected_site_id},function(d){
self.cfeeds.removeAll();
$.each(d['configured_feeds'],function(i,v) {
self.cfeeds.push(new Feed(v));
});
setTimeout(vm.loadFeeds,5000);
});
};
...
$(function(){
vm.loadFeeds();
});
...
It seems to me that the knockout rendering should occur at the atomic TD level only when a bound value changes, rather than at the TR level as it appears to be doing.
Hence, my question is: Is there a way to keep knockout from rerendering the entire row in a foreach, and instead only rerender the TD cells that have a data-bind attribute and leave my D3 charts alone?

Related

Drilldown functionality with AngularJS using ng-repeat property and jQuery

I want to do the exact same thing as this Fiddle (The example is mine) but using angular. In the normal HTML I have a parent tr with its own child tr so when I run the example I can see
that the child records that are related to its parent tr
Then I start doing this in Angular but now Im confused because I'm using ng-repeat and a JSON structure to populate the data so now I only have this piece of code, I put this as the parent but I don't know how to deal with the "child":
<tbody>
<tr ng-repeat="d in category" class="parent">
<td class="expand"></td>
<td ng-bind="d.cat"></td>
<td ng-bind="d.LW$"></td>
<td ng-bind="d.LW"></td>
<td ng-bind="d.L4W"></td>
<td ng-bind="d.L13W"></td>
<td ng-bind="d.L52W"></td>
</tr>
</tbody>
Here is my code with the angular version: https://jsfiddle.net/228wkfej/

need advice on dust temmplate

I am completely new to dust (linkedin), just working on my first little template. After writing it the obvious (but long) way I thought of a way to optimize using an inline partial.
The long version looks like this:
{#parcours}<tr class="pcsel_pc" id="{id}">
<td class="pcsel_exp_btn"><a href="#" class="list{?exp}Hide{:else}Exp{/exp}Btn">
<span class="glyphicon glyphicon-{?exp}minus{:else}plus{/exp}"></span></a></td>
<td class="pcsel_col">{name}</td><td class="pcsel_col pcsel_num">{count}</td>
</tr>
{?exp}
{#variants}
<tr class="pcsel_var{?sel} pcsel_sel{/sel}" id="{id}" >
<td class="pcsel_col"> </td><td class="pcsel_var pcsel_col">{name}</td>
<td class="pcsel_col pcsel_num">{count}</td>
</tr>
{/variants}
{:else}
{#variants}
<tr class="pcsel_var pcsel_hide" id="{id}" >
<td class="pcsel_col"> </td><td class="pcsel_var pcsel_col">{name}</td>
<td class="pcsel_col pcsel_num">{count}</td>
</tr>
{/variants}
{/exp}
{/parcours}
Explanation:
I have a context parcours that contains an inner context variants. If the variable exp does not exist in the outer context, I want to use a class pcsel_hide in the inner context.
This solution works but the code for the inner context is contained twice which is kind of stupid. So I thought of a way to use an inline partial which is conditionally set in the outer context and used in the inner context:
{#parcours}<tr class="pcsel_pc" id="{id}">
<td class="pcsel_exp_btn"><a href="#" class="list{?exp}Hide{:else}Exp{/exp}Btn">
<span class="glyphicon glyphicon-{?exp}minus{:else}plus{/exp}"></span></a></td>
<td class="pcsel_col">{name}</td><td class="pcsel_col pcsel_num">{count}</td>
</tr>
{?exp}{<hide/}{:else}{<hide} pcsel_hide{/hide}{/exp}
{#variants}
<tr class="pcsel_var{+hide/}{?sel} pcsel_sel{/sel}" id="{id}" >
<td class="pcsel_col"> </td><td class="pcsel_var pcsel_col">{name}</td>
<td class="pcsel_col pcsel_num">{count}</td>
</tr>
{/variants}
{/parcours}
This version is nice and short, but it doesn't seem to do the job. I see the class pcsel_hide all the time even if the outer context contains exp and thus uses the correct classes.
Any ideas ?
This is because inline partials are statically evaluated before template rendering begins. The last version of an inline partial defined with the same name wins.
Inline partials cannot be conditionally evaluated like this. What you probably want instead is to use a logic helper like {#eq}.

Angular append directive template to table

I have situation when i need to repeat multiple tbody in one table, what im trying to do is to make every tbody directive and i want its template to append to table, but when im put the directive inside the table tag its put his content outside the table.
the cart draw directive:
return {
restrict : 'AE',
templateUrl: 'client/cart/views/cart-draw.html',
scope : {},
replace: true,
controller : controller
}
the tpl:
<tbody ng-repeat="draw in CartService.items.draws track by $index">
<tr>
<td>
//some content
</td>
</tr>
</tbody>
the html:
<table class="table">
<cart-draw></cart-draw>
</table>
here is the plunker, if you inspect element you will see the tbody is out of the table:
http://plnkr.co/edit/9wEGFE5K0w0ayp6qo8Lx?p=preview
That is happening because the <table> tag doesn't recognize your custom <cart-draw> element as a valid child.
I would modify like so: http://plnkr.co/edit/u88N76h5dvLAvR3C1kRs?p=preview
index.html
<table><tbody cart-draw></tbody></table>
cart-draw.html
<tbody ng-repeat="body in bodies">
<tr>
<td>
{{body}}
</td>
</tr>
</tbody>
app.js
$scope.bodies = ["hello1", "hello2", "hello3"];
This is a long pending issue in Angular's Github repo.
https://github.com/angular/angular.js/issues/1459
I also stumbled upon to this problem once (with SVG). It happens because before rendering the directive, the template is cross verified with HTML DTD and alone doesn't make sense (without tag) and so it doesn't work. Same applies to <tr> and <li>
There are many solutions which uses ng-transclude and link functions to wrap it in respective parent tag and then use it.
This is actually a known & strange issue when it comes to directives & <table>'s.
I believe it actually comes in as invalid HTML at first, causing it somehow appear outside of your <table> tag.
Try making cart-draw an attribute of a <tbody>:
<table>
<tbody cart-draw></tbody>
</table>
plunker Example
This will make it work as intended.

Finding an ng-repeat index?

I can do this in Angular.js:
<tr ng-repeat="cust in customers">
<td>
{{ cust.name }}
</td>
</tr>
Which would iterate through all the customers by putting each in a <tr> of its own.
But what if I wanted two customers in one <tr>? How would I accomplish that?
I normally do that by messing around with indexes and modulus values, but I'm not sure how to do that here.
It turns out this can be done without any custom filters or changing the format of your data, though it does require some extra markup. I thought this woudn't be possible at first as you can't use a span or anything similar within the table for your ng-repeat. This answer however, points out that you can use a tbody.
So it's just a case of using the ng-if directive (which renders html if the expression is true), the $index variable (provided by ng-repeat) and the $even variable (which is also provided by ng-repeat and is true when $index is even
I've created a demo in this Plunker
<div ng-controller="MainCtrl">
<table>
<tbody ng-repeat="cust in customers">
<tr ng-if="$even">
<td>{{customers[$index].text}}</td>
<td>{{customers[$index+1].text}}</td>
</tr>
</tbody>
</table>
</div>
This would of course only work if you have two columns, what if you have more? Well you can also put a full expression into ng-if rather than just a variable. So you can use modulus values like this:
<tbody ng-repeat="cust in customers">
<tr ng-if="($index % 3) == 0">
<td>{{customers[$index].text}}</td>
<td>{{customers[$index+1].text}}</td>
<td>{{customers[$index+2].text}}</td>
</tr>
</tbody>

How to show CompositeView with multiple children view in Backbone Marionette

The Starting Problem
I have a CompositeView (a table) for which each model in the collection is represented as two table rows, with a template like:
<tr class="row-parent">
<td>parent info here</td>
</tr>
<tr class="row-child">
<td>child info here</td>
</tr>
With an ItemView like this:
var ItemView = Backbone.Marionette.ItemView.extend({
template: ItemTmpl
});
Even though they are named 'parent' and 'child', they are actually peer members of the same model. If I don't specify a tagName, Backbone will wrap each view in a <div> which is both invalid HTML and also breaks the layout.
The First Attempt at a Solution
So I figured, why not remove the outer <tr> tags and let Backbone add them in. So I updated my template to be like:
<td>parent info here</td>
</tr>
<tr class="row-child">
<td>child info here</td>
And updated the view to:
var ItemView = Backbone.Marionette.ItemView.extend({
template: ItemTmpl,
tagName: 'tr',
className: 'row-parent'
});
I was hoping that an outer tag would combine with the inner tag fragments, but Marionette didn't like that. It only showed the row-child. So I'm not sure where to go from here. I'm considering two strategies but haven't gone into much details yet.
Moving Forward: Plan A
Override whatever part of Backbone creates the extra div to not create it, or override the part of Marionette which appends the view to remove the div just before appending.
Moving Forward: Plan B
Create a new type of view called CompositeMultiView which, naturally, would extend off CompositeView and allow you two specify a second ItemView, or maybe just an array of views, all of which would be rendered for each model given. This plan seems like a lot more work but less hacked.
Does anyone have any better suggestions, workarounds or concrete pointers as to how I would go about implementing either of the two above plans?
Here is a mockup of what the table should look like:
I struggled with that same problem until I finally discovered today, that a table can have multiple tbody tags, each one containing multiple tr tags.
This is actually the answer provided to a similar backbone question.
So your ItemView would become:
var ItemView = Backbone.Marionette.ItemView.extend({
template: ItemTmpl,
tagName: 'tbody'
});
And the generated html:
<table>
<!-- first item -->
<tbody>
<tr class="row-parent">
<td>parent info here</td>
</tr>
<tr class="row-child">
<td>child info here</td>
</tr>
</tbody>
<!-- second item -->
<tbody>
<tr class="row-parent">
<td>parent info here</td>
</tr>
<tr class="row-child">
<td>child info here</td>
</tr>
</tbody>
...
</table>
You could try modifying the CompositeView as follows:
Specify itemView as an array of views
Override addChildView to render each view for each model
This solution ends up looking a lot like your "Plan B". Give it a shot:
itemView: [My.ParentView, My.ChildView],
addChildView: function(item, collection, options){
this.closeEmptyView();
var itemViews = this.getItemView(item);
var index = this.collection.indexOf(item);
_.each(itemViews, function(ItemView) {
this.addItemView(item, ItemView, index);
});
}
I haven't thought through whether this would handle model events such as destroy, but I believe it should handle them gracefully.
Dirty solution: Add a custom render function to your ItemView
// Or whatever template you use
var template = Handlebars.compile(datepickerTemplate);
var ItemView = Backbone.Marionette.ItemView.extend({
render: function(){
var html = template(this.model.toJSON());
var newElement = $(html);
this.$el.replaceWith(newElement);
this.setElement(newElement);
return this;
}
});
This should remove the extra div wrapping

Categories

Resources