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
Related
I have an angular app with one controller and one view, in my view I have a navigation bar with four options, the thing is that I cant create more html files, I need to work all in the index.html file that is under my app folder, in that file I have three tables where I bind some data coming from a WS that I call in three differente functions in my controller file, for example like this:
<table class="table table-bordered" ng-if="topTable === 'topCategory'">
<tbody>
<tr ng-repeat="d in data">
<td ng-bind = "d.Name">
<td ng-bind = "d.Email">
</tr>
</table>
<table class="table table-bordered" ng-if="topTable === 'topBrand'">
<tbody>
<tr ng-repeat="d in data">
<td ng-bind = "d.Name">
<td ng-bind = "d.Email">
</tr>
</table>
I hide the firs two and depending on the radio button selection that I have other tables appears.
My issue is that as I mentioned before, my navigation bar has four options where I have three more tables for each option, so I saw something about directives but Im new to angular and I want to know how to render all the html that I have in my current view without losing functionality and allowing the app to render the correct data depending on the selected option of the nav bar.
I really need help on this.
Thanks
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?
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.
UPDATE: After some very insightful code from #Marc Kline, I went back and cleaned up my page. It turned out that I had my controllers listed in reversed (My angular controller was inside the Isotope controller, instead of the other way round). Once I changed it back and cleaned off some additional scripting, it started working again. I have updated the code snippet to reflect the change. Thanks to Marc and S.O!
I am having trouble figuring out how can I add new items using Angular and still let Isotope manage their UI display.
I am using Isotope and Angular to render server results in a masonry style layout. When I add new items to the layout on a button click, angular adds it just fine. However, they do not appear in the context of the isotope UI and appear separately (and cannot be sorted, laid out or filtered using Isotope).
Here is my JS Code
<!-- Define controller -->
var contrl = app.controller("MainController", function($scope){
$scope.items ={!lstXYZ}; //JSON data from server
//Function called by button click
$scope.addItem = function(item)
{
$scope.items.push(item);
$scope.item = {};
}
});
contrl.$inject = ['$scope'];
Here is the HTML to display the server results...(Updated to show working code..refer comments)
<div ng-controller="MainController">
<div class="isotope" id="isotopeContainer">
<div ng-repeat="item in items">
<div class='element-item {{item.status}}' data-category='{{item.status}}'>
<p class="number">{{item.type}}</p>
</div>
</div>
</div>
</div>
And here is my HTML button to add the new items
<table>
<tr>
<td><input type="text" ng-model="item.status" /></td>
</tr>
<tr>
<td><input type="text" ng-model="item.type" /></td>
</tr>
<tr>
<td colspan="2"><input type="Button" value="Add" ng-click="addItem(item)" /> </td>
</tr>
</table>
I am not sure how do I ensure that Isotope can recognize the newly added element and re-animate the layout.
Any help or pointers will be very appreciated. Thanks!
ng-repeat takes care of adding the new element to the DOM for you. However, Isotope isn't doing any "watching" for you - you have to manually invoke a redraw of the container.
You could just add something like $("#isotopeContainer").isotope(...) directly to your controller, but in the spirit of keeping your controllers lean and free of DOM-related code, you should instead create a service. Something like:
myApp.service('Isotope', function(){
this.init = function(container) {
$(container).isotope({itemSelector: '.element-item'});
};
this.append = function(container, elem) {
$(container).isotope('appended', elem);
}
});
... where the first method initializes a new Isotope container and the next redraws the container after an item is appended.
You could then inject this service into any controller or directive, but directives probably are best for this scenario. In this Plunker, I created two new directives: one for Isotope containers and another for Isotype elements, and used the service to do the initialization and redrawing.
In this particular case, my code was not written correctly. I have updated the question's code but wanted to mention it more clearly here...
Apparently, the beauty of Angular is that you do not need to bother with the underlying UI framework (Isotope in this case). As long as you update the Angular data array, the UI binding is updated automatically.
The only gotcha is to ensure that the UI framework div is within the context of your Angular div.
Here is the non-working code...Note that the isotope div is outside the Angular controller.
<div class="isotope" id="isotopeContainer">
<div ng-controller="MainController">
<div ng-repeat="item in items">
<div class='element-item {{item.status}}' data-category='{{item.status}}'>
<p class="number">{{item.type}}</p>
</div>
</div>
</div>
</div>
Here is the updated code with isotope running within the Angular controller context...
<div ng-controller="MainController">
<div class="isotope" id="isotopeContainer">
<div ng-repeat="item in items">
<div class='element-item {{item.status}}' data-category='{{item.status}}'>
<p class="number">{{item.type}}</p>
</div>
</div>
</div>
</div>
Hope this helps. I am thankful for all the responses and help I got from SO. Appreciate the learning opportunity.
I wanted to have the functionality of rearranging rows in a table (sorting rows using drag and drop).
And the index of the row arrangement should also change in the model.
How can I do something similar to this : http://jsfiddle.net/tzYbU/1162/
using Angular Directive?
I am generating table as :
<table id="sort" class="table table-striped table-bordered">
<thead>
<tr>
<th class="header-color-green"></th>
<th ng-repeat="titles in Rules.Titles">{{titles.title}}</th>
</tr>
</thead>
<tbody ng-repeat="rule in Rules.data">
<tr>
<td class="center"><span>{{rule.ruleSeq}}</span></td>
<td ng-repeat="data in rule.ruleData">{{statusArr[data.value]}}</td>
</tr>
</tbody>
</table>
I did it. See my code below.
HTML
<div ng:controller="controller">
<table style="width:auto;" class="table table-bordered">
<thead>
<tr>
<th>Index</th>
<th>Count</th>
</tr>
</thead>
<tbody ui:sortable ng:model="list">
<tr ng:repeat="item in list" class="item" style="cursor: move;">
<td>{{$index}}</td>
<td>{{item}}</td>
</tr>
</tbody>{{list}}
<hr>
</div>
Directive (JS)
var myapp = angular.module('myapp', ['ui']);
myapp.controller('controller', function ($scope) {
$scope.list = ["one", "two", "thre", "four", "five", "six"];
});
angular.bootstrap(document, ['myapp']);
There is another library: RubaXa/Sortable: https://github.com/RubaXa/Sortable
It is for modern browsers and without jQuery dependency. Included is a angular directive. I'm going to check it out now.
You get good touch support additionally.
AngularJS was not really built for the manipulation of DOM elements, rather to extend the HTML of a page.
See this question and this Wikipedia entry.
For DOM manipulation, jQuery/mootools/etc will suite you just fine (hint: the example in your jsFiddle link).
You could probably use AngularJS to keep track of the ordering of your elements to update your model. I'm not sure how to do this using directives, but the following code may be useful
var MyController = function($scope, $http) {
$scope.rules = [...];
...
}
var updateRules = function(rule, position) {
//We need the scope
var scope = angular.element($(/*controller_element*/)).scope(); //controller_element would be the element with ng-controller='MyController'
//Update scope.rules
}
Then when you reorder the list, simply call updateRules() with the changed rule and its new position in the model.
Anyone else who wants something like this but not understanding the accepted answer. Here is a directive UI.Sortable for AngularJS that allows you to sort an array/ table rows with drag & drop.
Requirements
JQuery v3.1+ (for jQuery v1.x & v2.x use v0.14.x versions)
JQueryUI v1.12+
AngularJS v1.2+
Usage
Load the script file: sortable.js in your application: (you can find this sortable.js from here
<script type="text/javascript" src="modules/directives/sortable/src/sortable.js"></script>
make sure you have included JQuery, AngularJs and JQueryUI js files in
order before this sortable file
Add the sortable module as a dependency to your application module:
var myAppModule = angular.module('MyApp', ['ui.sortable'])
Apply the directive to your form elements:
<ul ui-sortable ng-model="items">
<li ng-repeat="item in items">{{ item }}</li>
</ul>
Developing Notes:
ng-model is required, so that the directive knows which model to update.
ui-sortable element should contain only one ng-repeat
Filters that manipulate the model (like filter, orderBy, limitTo,...) should be applied in the controller instead of the ng-repeat
3rd point is very Important as it took almost an hour to understand
why my sorting was not working?? It was because of orderBy in html
and that was resetting the sorting again.
For more understanding you can check the detail here.
If I understand you correctly, you want to be able to sort the rows? If so, use UI-Sortable: GitHub
Demo: codepen.io/thgreasi/pen/jlkhr
Along with RubaXa/Sortable there is one more angularjs library avilable that is angular-ui-tree. Along with drag and drop we can arrange elements in a tree structure and we can add and delete elements (nodes)
Please see the this link for examples
http://angular-ui-tree.github.io/angular-ui-tree/#/basic-example .
Please see this for github
https://github.com/angular-ui-tree/angular-ui-tree.