backbone js - table pagination updating all visited views - javascript

I'm quite new to backbone so there could be a really simple solution to this problem. I have an app where you can view a show page of which there is a table I'm adding pagination to. I have created a utility object Table to handle the pagination so it can be used on every table on each show page:
var Table = function(rowsStart, increment, data) {
this.rowsStart = rowsStart;
this.increment = increment;
this.data = data;
this.totalRows = _.size(data);
this.totalRowsRoundUp = Math.ceil(_.size(data)/10)*10;
this.paginate = function(paginateVol) {
// Scope the figures
this.rowsStart += paginateVol
this.increment += paginateVol
rS = this.rowsStart;
inc = this.increment;
// Show first increment results
var rowsToDisplay = [];
$.each(this.data, function(i,e){
if (i < inc && i >= rS) {
rowsToDisplay.push(e)
}
});
// Send back the rows to display
return rowsToDisplay
};
}
This works fine when visiting the first show page table in the backbone history but when I visit further show pages and action this pagination object it triggers on all visited table views and produces weird results on my current view.
My View look like this:
// Create a view for the outer shell of our table - not inclusive of rows
queriesToolApp.ReportKeywordsView = Backbone.View.extend({
el: '#report-queries-js',
events: {
'click #prev' : 'clickBack',
'click #next' : 'clickNext'
},
initialize: function() {
// compile our template
this.template = _.template($('#tpl_indiv_report').html());
// create an instance of the Table paginator object
this.paginator = new Table(0, 10, this.collection.attributes);
},
render: function(paginateVol) {
// Scope this
_this = this;
var data = _this.collection.attributes;
// Render the script template
this.$el.html(this.template());
// Select the table body to append
var tableBody = $('#report-queries-row');
// Store the keyword object and the pagination amount
var keywordObj = this.paginator.paginate(paginateVol);
// Append keyword data to page
$.each(keywordObj, function(index, keyword){
// Create a new instance of the individual view
var reportKeywordIndView = new queriesToolApp.ReportKeywordIndView({model: keyword})
// append this to the table
tableBody.append(reportKeywordIndView.render().el);
// start table listen when last row appended
});
},
clickBack: function() {
// Render the view passing in true as it's going back
this.render(-10);
},
clickNext: function() {
// Render the view passing in nothing as default is forward
this.render(10);
}
})
Here is the individual view:
queriesToolApp.ReportKeywordIndView = Backbone.View.extend({
tagName: 'tr',
className: 'table-row',
initialize: function() {
// Define the template and compile the template in the view
this.template = _.template($('#tpl_indiv_report_row').html());
},
render: function() {
// Set the view with the data provided from the model
this.$el.html(this.template(this.model));
return this;
}
})
And I start backbone here:
$(function() {
// create a new instance of the router
var router = new queriesToolApp.AppRouter();
// Record the history of the url hash fragments
Backbone.history.start();
});
Is there a way around this?

I think to make it work keeping the objects you defined, I'd be helpful to see the AppRouter code. One thing seems certian: the ReportKeywordIndView instances keep getting created but probably not being deleted. This could work a different way, without that extra view.
As its written here, the row view looks too simple to be a backbone view. You could probably fix this by changing the template to include the TR tag, remove the view and make some adjustments in the main view.
This can go in the initialize function:
this.rowTemplate = _.template($('#tpl_indiv_report_row').html());
This can replace the each code which creates the row view and adds it:
// append this to the table
tableBody.append(_this.rowTemplate(keyword));

Related

Backbone.Marionette Layout: Access region.layout.view directly

I have a Marionette application which as more than 1 region.
App.addRegions({
pageRegion: '#page',
contentsRegion :'#contents'
});
As part of App.pageRegion, I add a layout.
App.ConfiguratorLayout = Marionette.Layout.extend({
template: '#configurator-page',
regions: {
CoreConfiguratorRegion: '#Core-Configurator-Region',
SomeOtherRegion:'#someOtherregion'
}
});
This layout is rendered before the application starts.
App.addInitializer(function() {
var configLayout = new App.ConfiguratorLayout();
App.pageRegion.show(configLayout);
});
Later on in the application, I just need to change the contents of the configLayout.
I am trying to achieve something like this.
App.pageRegion.ConfiguratorLayout.CoreConfiguratorRegion.show(someOtherLayout);
Is there a way to do this besides using DOM selector on the $el of App.pageRegion.
App.pageRegion.$el.find('#...')
Instead of in an app initializer, move the configLayout initialization code into a controller that can keep a reference to it and then show() something in one of its regions.
// pseudocode:
ConfigController = Marionette.Controller.extend({
showConfig: function() {
var layout = new App.ConfiguratorLayout();
App.pageRegion.show(configLayout);
var someOtherLayout = new App.CoreConfiguratorLayout();
layout.coreConfiguratorRegion.show(someOtherLayout);
// ... maybe create and show() some views here?
}
});
App.addInitializer(function() {
var controller = new ConfigController();
// more likely this would be bound to a router via appRoutes, instead of calling directly
controller.showConfig();
});

Backbone.js Form Values Remembered in History

I'm new to the whole Backbone (and I'm actually not much of a JavaScript programmer yet), and I'm running into issues with a filter form.
Basically I have a database that my backbone collection gets populated from. On the page, there is a filter section where I can checkmark things such as: "Jobstatus: Running, Completed, Failed, etc..." Or input such as "User name: test0." All that works great until I started paginating my results.
I have a router set up to handle #page/:page. This works as well, say I go to page 2 with no filters checked, and then on page 2 I check some filters (which right now sets my results back to page 1 by using navigate(page/1)), when I hit my back button, I go back to page 2 (since that is where my no filter results were last on), but my filter box is still checked.
So if there are 10 rows per page and I have 14 results with no filters. I go to page 2 to see results 11-14, check a filter that returns me to page 1 with a total of 5 results, and then i hit back. I am now on page 2 of those filtered results seeing rows 11-5 of 5...
Is there anyway for the history to remember the form checkboxes and other inputs? So if I hit back in the situation I describe, it goes to page 2, but with the form filters removes as they were prior.
I was thinking I'd have to use routes for every value on the form... but hoping there is a better way of doing this (hopefully without rewriting the entire code as well).
Any help is appreciated.
I have two main views for input: one tied to the filter form, one for the pagination selection. The filter form view calls another view for the results (which is ties to a collection of rows).
Here is the filter form view:
var FilterForm = Backbone.View.extend({
events: {
"submit": "refreshData",
"change input": "refreshData"
},
initialize: function () {
this.refreshData();
},
refreshData: function () {
Backbone.history.navigate('page/1');
pageNumberModel.set('pageNumber', 1);
this.newPage();
},
newPage: function () {
var pageNumber = pageNumberModel.get('pageNumber');
var rowsPerPage = pageNumberModel.get('rowsPerPage');
var startRow = ((pageNumber * rowsPerPage) - rowsPerPage) + 1;
var endRow = startRow + (rowsPerPage - 1);
$('#startrow').val(startRow);
$('#endrow').val(endRow);
var inputData = this.$el.serializeArray();
var inputDatareduced = _(inputData).reduce(function (acc, field) {
acc[field.name] = field.value;
return acc;
});
$.get("Database.aspx", inputData, function (outputData) {
jobQueueRows.set($.parseJSON($.parseJSON(outputData)['JobQueue']));
jobQueueRowsView.render();
pageNumberModel.set($.parseJSON($.parseJSON(outputData)['Pagination']));
pageNumberView.render();
rowNumberView.render();
});
}
});
I would recommend creating a filterModel for your FilterForm view and using that to hold the state. Here is a reference implementation.
FilterModel:
var FilterModel = Backbone.Model.extend({
initialize: function(){
if( !this.get('filters'))
this.set({filters: new Array()});
},
applyFilter: function(key){
var filters = this.get("filters");
set("filters", _.union(filters, key));
return this;
},
removeFilter: function(key){
var filters = this.get("filters");
set("filters", _.without(filters, key));
return this;
},
defaults: {
fooFilters: [
{key: "size", label: "Filter By Size"},
{key: "color", label: "Past Week"}]
}
});
Bind a handler in your view to update the filter model:
filterClicked: function (e) {
var filter = $(e.target);
if(filter.is(':checked'))
this.model.applyFilter(filter.attr("id"));
else
this.model.removeFilter(filter.attr("id"));
},
Modify your filter template to apply the models state to your checkboxes when rendering:
<ul id="fooFilters" class="foo-filter-list">
<% _.each(fooFilters, function(f) { %>
<li>
<label for="<%=f.key%>"><%=f.name%></label>
<input type="checkbox" name="" value="" id="<%=f.key%>" <%if($.inArray(f.key, filters) === -1 {%>checked<%}%>/>
</li>
<% }); %>
</ul>

Backbone events not firing on new element added to DOM

I'm using a combination of handlebars and Backbone. I have one "container" view which has an array to hold child views. Whenever I add a new view, click events are not being bound.
My Post View:
Post.View = Backbone.View.extend({
CommentViews: {},
events: {
"click .likePost": "likePost",
"click .dislikePost": "dislikePost",
"click .addComment button": "addComment"
},
render: function() {
this.model.set("likeCount", this.model.get("likes").length);
this.model.set("dislikeCount", this.model.get("dislikes").length);
this.$('.like-count').html(this.model.get("likeCount") + " likes");
this.$('.dislike-count').html(this.model.get("dislikeCount") + " dislikes");
return this;
}, ...
My callback code in the "container" view which creates a new backbone view, attaches it to a handlebars template and shows it on the page:
success: _.bind(function(data,status,xhr) {
$(this.el).find("#appendedInputButton").val('');
var newPost = new Post.Model(data);
var newPostView = new Post.View({model: newPost, el: "#wall-post-" + newPost.id});
var source = $("#post-template").html();
var template = Handlebars.compile(source);
var html = template(newPost.toJSON());
this.$('#posts').append(html);
newPostView.render();
this.PostViews[newPost.id] = newPostView;
}, this), ...
Not sure what's going on, but this sort of code is run initially to set up the page (sans handlebars since the html is rendered server-side) and all events work fine. If I reload the page, I can like/dislike a post as well.
What am I missing?
I dont see you appending newPostView.render().el to dom .Or am i missing somehting?
Assuming the "#post-template" contains the "likePost" button. The newPostView is never added to the DOM.
Adding el to the new Post.View makes backbone search the DOM (and the element won't exist yet)
4 lines later a HTML string is added to the DOM (assuming the this.el is already in the DOM)
If you create the Post.View after the append(html) the element can be found and events would be fireing.
But the natural Backbone way would be to render the HTML string inside the Post.View render function, append the result to it's el and append that el to the #posts element.
success: function (data) {
var view = new Post.View({model: new Post.Model(data)});
this.$('#posts').append(view.render().el);
this.PostViews[data.id] = view;
}

Why are all item being removed from my Backbone.js view when I click the X

js. I would like to know why my backbone.js is removing both items when I click on the X next to the album and artist.
i.e.
The Chronic-- Dr. Dre-- X
Ten-- Pearl Jam-- X
I appreciate any feedback that I could get that would allow me to only remove one item as opposed to both
Javacript:
(function($){
Backbone.sync = function(method, model, success, error){
success();
}
//Declare model
var Album = Backbone.Model.extend({
defaults: {
album: 'Logical Progresions vol. 1',
artist:'LTJ Bukem'
}
});
//Declare collection
var eList = Backbone.Collection.extend({
model: Album
});
//Declare the view for the Albums
var AlbumView = Backbone.View.extend({
el: $('div#main'),
template: _.template(
"<div class=\"alert\"> " +
" <span class=\"album\"><%= album %>--</span> " +
" <span claas=\"artist\"><%= artist %>--</span> " +
" <span class =\"delete\">X</span> " +
"</div>"
),
events: {
'click span.delete': 'deleteAlbum'
},
initialize: function(){
_.bindAll(this, 'render','unrender','deleteAlbum');
this.model.bind('remove', this.unrender);
},
// `unrender()`: Makes Model remove itself from the DOM.
unrender: function(){
this.$el.remove();
},
deleteAlbum: function(){
this.model.destroy();
},
render: function(){
$(this.el).append(this.template(this.model.toJSON()));
}
});
var appendItem = function(item){
var albumView = new AlbumView({
model: item
});
albumView.render();
}
//// set the stuff in motion
var elist = new eList();
elist.bind("add",function(listItem){appendItem(listItem)});
elist.add({
album: 'The Chronic',
artist: 'Dr. Dre'
});
elist.add({
album: 'Ten',
artist: 'Pearl Jam'
});
})(jQuery);
There are a few things I can point out.
First, your view - when you create multiple instances for each album - each share the same el. That is, div#main. Each time you add one, you're appending the template stuff to the el which is why you still see the other. But when you click on .delete and execute the this.$el.remove() you are removing everything in the el. Which includes the other view.
You should separate it out, each view should have it's own unique el.
el: 'div',
className: 'albumView'
When you add each album view, you can create the view and append it to your div#main
var view = new AlbumView();
$('#main').append(view.render().el); // the el refers to the subview (albumView) el.
This should keep each view happy with its own el and removal will only affect that view/model/DOMelement.
UPDATE - context of $('#main').append(view.render().el);
Basically, when you create the albumViews and append them the most ideal place to do this is in the larger context in which your div#main exists. For example, this might happen in your main js script in the header, or maybe even in a larger view that contains many albumView subviews. To illustrate the subview context:
var ParentView = Backbone.View.extend({
el: $('#main'),
render: function() {
this.addAllAlbums(); // On rendering the parent view, we add each album subview
return this;
},
addAllAlbums: function() {
var self = this;
// This loops through the collection and makes a view for each album model
this.collection.each(function(albumModel) {
self.addAlbumView(albumModel);
});
},
addAlbumView: function(albumModel) {
var view = new AlbumView({
'model': albumModel
});
// We are adding/appending each albumView to this view's el
this.$el.append(view.render().el);
// ABOVE: `this.$el` refers to ParentView el. view.render().el refers to the
// albumView or subview el.
// As explained before, now each album view has it's own el which exists in
// the parent view's this.el = `$('#main')`
}
});
// We create the parent BIG/ALLAlbumsView and toss into it the collection of albums
var BigAlbumsView = new ParentView({
'collection': albumsCollection
});
BigAlbumsView.render(); // Run the `render()` to generate all your album subviews
You might also want to store reference to those subviews by adding these lines in your code of the parent view. It will make cleaning up easier although it's not a big deal if you intend on cleaning up individual views through the subviews themselves.
// In your initialization, we create an array to store album subviews
this.albumViews = [];
// In `addAlbumView()` we push each view into the array so we have a reference
this.albumViews.push(view);
// When cleaning up, you just simply cycle through the subviews[] and remove/close
// each album subview
_.each(this.albumViews, function(albumView) {
albumView.$el.remove();
});
Hope this helps.
PS - Last note that I noticed. When you remove a view, I noticed you use remove() which is the way to get it out of the DOM. If you're making more complex subviews intertwined/tangled with event listeners to collections, models, and other views - you might want to read Derick Bailey's take on Zombie views and implementing a close() method that will both remove() and unbind() your view so there are no references to it and it can be garbage collected. Not the focus of this question but good for extra credit and possibly relevant since this has probably made your code more complicated. :-P
Removing Views - avoiding zombies

backbone view events don't work after re-render

I'm pulling my hair out, I cannot seem to get mouse events to work on my backbone view after the view is re-rendered unless i do the most ridiculous thing:
$("a").die().unbind().live("mousedown",this.switchtabs);
I actually had this in there but decided to update to the latest backbone and try to use the new delegateEvents()function.
Here is the way my project id structured:
Appview / AppRouter
|
----->PageCollection
|
------->PageView/PageModel
------->PageView/PageModel these page view/models are not rendered
------->PageView/PageModel
|
------->PageView/PageModel
|
----->render() *when a pageview is rendered*
|
-----> Creates new
Tabcollection
|
--->TabModel/TabView <-- this is where the issue is
What happens is that the tabcollection has a main tabview to manage all of the tabs, then creates a new model/view for each tab and puts a listener to re-render the tabview whenever a tab is loaded. If the tabview is re-rendered, no mouse events work anymore unless I put that contrived jQuery statement in there.
Heres the tabview and render (ive stripped it down quite a bit)
var TabPanelView = Backbone.View.extend({
className: "tabpanel",
html: 'no content',
model: null,
rendered: false,
events:{
'click a.tab-nav': 'switchtabs'
},
initialize: function(args)
{
this.nav = $("<ol/>");
this.views = args.items;
this.className = args.classname?args.classname:"tabpanel";
this.id = args.id;
this.container = $("<section>").attr("class",this.className).attr("id",this.id);
_.bindAll(this);
return this.el
},
/*
This render happens multiple times, the first time it just puts an empty html structure in place
waiting for each of the sub models/views to load in (one per tab)
*/
render: function(args){
if(!args)
{
//first render
var nav = $("<aside/>").addClass("tab-navigation").append("<ol/>").attr("role","navigation");
var tabcontent = $("<section/>").addClass("tab-panels");
for(i = 0;i<this.views.length;i++)
{
$("ol",nav).append("<li><a rel='"+this.views[i].id+"' href='javascript:;' class='tab-nav'></a></li>");
tabcontent.append(this.views[i].el);
}
this.$el.empty().append(nav).append(tabcontent);
}
else if(args && args.update == true){
// partial render -- i.e. update happened inside of child objects
var targetid = args.what.cid;
for(i = 0;i<this.views.length;i++)
{
var curcontent = this.$el.find("div#"+this.views[i].id);
var curlink = this.$el.find("a[rel='"+this.views[i].id+"']")
if(this.views[i].cid == targetid)
{
curcontent.html($(this.views[i].el).html());
curlink.text(this.views[i].model.rawdata.header);
}
if(i>0)
{
// set the first panel
curcontent.addClass("tab-content-hide");
}
if(i==0)
{
curcontent.addClass("tab-content-show");
curlink.addClass("tab-nav-selected");
}
// this ridiculous piece of jQuery is the *ONLY* this i've found that works
//$("a[rel='"+this.views[i].id+"']").die().unbind().live("mousedown",this.switchtabs);
}
}
this.delegateEvents();
return this;
},
switchtabs: function(args){
var tabTarget = args.target?args.target:false
if(tabTarget)
{
this.$el.find("aside.tab-navigation a").each(function(a,b)
{
$(this).removeClass("tab-nav-selected")
})
$(tabTarget).addClass("tab-nav-selected");
this.$el.find("div.tab-content-show").removeClass("tab-content-show").addClass("tab-content-hide");
this.$el.find("div#"+tabTarget.rel).removeClass("tab-content-hide").addClass("tab-content-show");
}
}
});
Can anyone think of why backbone mouse events simply don't fire at all, is it because they are not on the DOM? I thought that this was where backbone was particularly useful?...
This line of code is likely your problem:
this.delegateEvents();
Remove that and it should work.
The only time you need to call delegateEvents yourself, is when you have events that are declared separately from your view's events hash. Backbone's view will call this method for you when you create an instance of the view.
When the view is being re-rendered, are you reusing the same view and just calling render() on it again, or are you deleting the view and creating a whole new view?
Either way, it looks like the cause is that the view events are not being unbound before the view is re-rendered. Derick Bailey has a great post about this.
When you re-render, 1) make sure you unbind all the events in the old view and 2) create a new view and render it
When using $(el).empty() it removes all the child elements in the selected element AND removes ALL the events (and data) that are bound to any (child) elements inside of the selected element (el).
To keep the events bound to the child elements, but still remove the child elements, use:
$(el).children().detach(); instead of $(.el).empty();
This will allow your view to rerender successfully with the events still bound and working.

Categories

Resources