I have large collection of json objects which I retrieve through a search function, though depends on the search string, the output can go up to more than thousand of arrays which I populate into a list. Within mobile environment this become a hassle and memory consuming once I add touchmove, touchstart and touchend to each object. I found solution to this that there's a minimal way of showing object using backbone.js and with trigger such as button this could become robust. though I don't know how to go foward with it. This is working example without the button. And how I shoud do this?
<script>
//model - define value objects.
var Client = Backbone.Model.extend({
defaults: {
name: 'cole',
age: '12'
}
});
//collection - load json
var ClientCollection = Backbone.Collection.extend({
defaults: {
model: Client
},
model: Client,
url: './json/test.json',
//override parse due to json format. point to "items"
parse: function (response, xhr) {
return response.items;
}
});
//view. init collection. listen for data to be loaded. render.
var ClientView = Backbone.View.extend({
initialize: function () {
this.collection = new ClientCollection();
this.collection.bind("reset", this.render, this);
this.collection.fetch();
},
render: function () {
//append to html here ...
//alert(this.collection.at(0).get("name"));
//alert(this.collection.length)
for (var i = 0; i < this.collection.length; i++) {
$('#append-el').append('<li>' + this.collection.at([i]).get("name") + '; ' + this.collection.at([i]).get("age") + '</li>')
}
}
});
var clientView = new ClientView();
</script>
<div id = "append-el"></div>
Add an event listener to your view pointing to your button with the events hash, something like this
,events {
"click #buttonID" : "fillCollection"// <- this is a method name
}
and then create this method and trigger a collection.fetch, like this
,fillCollection: function(){
this.collection.fetch();
}
If i understood you well then this should work:
var ClientView = Backbone.View.extend({
el: '#append-el',
events: {
'click button': 'onButtonClick'
},
initialize: function() {
_.bindAll(this);
this.collection = new ClientCollection();
this.collection.bind("reset", this.renderClients);
this.render();
},
render: function() {
//append to html here ...
this.$el.append('<button type="button">Fetch clients</button><ul class="clients"></ul>');
},
renderClients: function() {
var $ul = this.$('ul.clients').empty();
this.collection.each(function(client) {
$ul.append('<li>' + client.get("name") + '; ' + client.get("age") + '</li>');
});
},
onButtonClick: function(e) {
this.collection.fetch();
}
});
I would suggest not to fetch thousands of items at once. Limit it to 100 max 200 hundred. Then I would start listening to scrolling on the list a fetch the rest of the items on as needed basis (you can automatically load them once the user approaches the end of the scrolled area or just place a "Load more" button at the bottom).
There are several paginator plugins for Backbone or you can simply limit the number of rendered element within the render() function.
Related
I created a view for table row, that i want to listen to any click on any cells in this row.
This is how I implemented:
let ListItem = Backbone.View.extend({
events: {
'click tr': 'showDetails',
'click': 'showDetails'
},
template: function() {
return "<tr><td><img src=<%= imageUrl %> /></td><td class='name'><%= firstName %></td><td><%= lastName %></td><td><%= homeTown %></td><td><button type='button' id='ddd' class='btn btn-danger'>Delete</button></td></tr>";
},
render: function() {
var oTemplate = _.template(this.template());
this.$el.html(oTemplate(this.model.toJSON()));
$('table > tbody:last-child').append(this.$el.html());
return this;
},
showDetails: function() {
alert("*****");
},
hide: function(bToHide, index) {
index++;
if (!bToHide) {
$($('tr')[index]).show();
return;
}
$($('tr')[index]).hide();
this.rendered = true;
},
openActorView: function() {
window.open('/#actor/' + window.actorsCollection.indexOf(this.model), '_self');
}
});
After clicking the row\cells within, nothing happens.
I listen to click on this view + click on the tr, but nothing happens.
Also, when add event to tr td nothing happens ether.
Thanks for helping
The reason it doesn't work
$('table > tbody:last-child').append(this.$el.html());
The jQuery .html() function returns a string.
So you're creating a nice Backbone view, with easy to define event listeners, then you don't use it, you just take its string representation and dump it in the DOM, losing any event listeners previously bound.
The improvements
First, there's no need to make the template a function, just use _.template directly. I wrote another answer which provides additional information on how to use _.template.
After that, don't use global selectors inside a view. It kills the purpose of a scoped component view. Let the parent view class deals with this.
let ListItem = Backbone.View.extend({
tagName: 'tr', // the list item is a `<tr>`
events: {
'click': 'showDetails'
},
template: _.template("<td><img src=<%= imageUrl %> /></td><td class='name'><%= firstName %></td><td><%= lastName %></td><td><%= homeTown %></td><td><button type='button' id='ddd' class='btn btn-danger'>Delete</button></td>"),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
showDetails: function() {
console.log("show details event");
}
});
Make a list view that manages the table. This is the parent class that will put the list items into the DOM.
Instead of using the jQuery core function, use the Backbone view scoped alias this.$() to cache the table body.
let ListView = Backbone.View.extend({
initialize: function() {
this.$body = this.$('tbody');
},
render: function() {
this.$body.empty();
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(model) {
var view = new ListItem({ model: model });
this.$body.append(view.render().el); // use the DOMElement
}
});
Then pass an existing table from outside the view.
new ListView({ el: 'table.my-table' }).render();
This is a super basic example and you should be aware that performance could be improved further and that it could leak memory if used as-is.
See how to improve list rendering performance with Backbone.
I have a simple backbone view as follows:
/**
* Renders a form view for an event object.
*/
APP.EventFormView = Backbone.View.extend({
tagName: 'form',
events: {
'keydown': 'keyPressed',
'focus input': 'inputChanged',
'change select': 'selectChanged',
'change textarea': 'textareaChanged'
},
initialize: function() {
this.template = _.template($('#newevent-form').html());
this.listenTo(this.model, 'change', this.render);
this.listenTo(APP.eventTypes, 'update', this.render);
this.listenTo(APP.selectedEvent, 'update', this.render);
},
render: function() {
var modelJSON = this.model.toJSON();
if ('id' in modelJSON && modelJSON.id !== "") {
this.loadForm();
} else if (!('id' in modelJSON) || modelJSON.id === "") {
this.loadForm();
} else {
this.$el.html('');
}
return this;
},
loadForm: function() {
var templateData = $.extend(this.model.toJSON(),
{"event_types":APP.eventTypes.toJSON()});
this.$el.html('');
this.$el.html(this.template($.extend(this.model.toJSON(),
{event_types: APP.eventTypes.toJSON()})));
$('.ev-main-container').html('').html(this.el);
},
inputChanged: function(e) {
console.log('inputChanged');
},
selectChanged: function(e) {
console.log('selectChanged');
},
textareaChanged: function(e) {
console.log('textareaChanged');
},
keyPressed: function(e) {
console.log('key pressed');
}
});
I initialize this view as follows under document.ready:
// Initialize the form view
APP.selectedEvent = APP.selectedEvent || new APP.Event();
APP.eventFormView = new APP.EventFormView({model: APP.selectedEvent});
APP.eventFormView.render();
But none of the events I have defined are firing for some reason, What is it that I am doing wrong here ?
Update:
Ok, I fugred out if i remove $('.ev-main-container').html('').html(this.el); from the loadForm method and instead intialize the view as follows, it works:
APP.eventFormView = new APP.EventFormView({
model: APP.selectedEvent,
el: $('.ev-main-container'),
});
I was able to resolve it but I still don't understand why this happens, could anyone throw a little light on what's going on and how this works.
jQuery's html function has a side effect that many people seem to forget about, from the fine manual:
jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
Consider what that means when you do something like this:
container.html(view.el);
container.html(view.el);
Everything will be fine after the first container.html() call. But the second will "remove ... event handlers from child elements" (such as view.el) before adding the new content. So after the second container.html() call, all the events on view.el are gone. Sound familiar?
You have lots of things that will call render on your view and render will eventually do this:
$('.ev-main-container').html('').html(this.el);
Your events will silently disappear the second time that gets called but the HTML will look just fine.
Consider this simplified example (http://jsfiddle.net/ambiguous/otnyv93e/):
var V = Backbone.View.extend({
tagName: 'form',
events: {
'click button': 'clicked'
},
initialize: function() {
this.template = _.template($('#t').html());
},
render: function() {
this.$el.html('');
this.$el.html(this.template());
$('.ev-main-container').html('').html(this.el);
return this;
},
clicked: function() {
console.log('clicked');
}
});
var v = new V;
v.render();
$('#re-render').click(function() {
v.render();
console.log('Re-rendered');
});
and you'll see exactly your problem.
If you make the view's el the .ev-main-container then you'll be using html() to alter the contents of el rather than altering the contents of the element that contains el. Once you're working entirely inside the el you're no longer accidentally re-using an element and no longer accidentally removing the event bindings from that element.
My rules of thumb for preventing event problems with Backbone:
Never attach views to existing DOM nodes, always let views create and own their own el and let the caller put that el in a container.
Call remove on views to dispose of them when they're no longer needed.
Don't try to re-use views, create them when you need them and remove them when you don't need them.
No view references anything outside its el.
There are exceptions (of course) and this approach won't solve everything but it is a good starting point and avoids most of the common problems.
Given a panel
var panel = new Backbone.CollectionView({...})
How do I get the current model being sorted?
panel.on('sortStart', function(e) {
var index = something;
});
I suppose you use some kind of UI manipulation tool for example jQuery UI. As Lesha said in her comment it can be done through triggering of event on the model view.
//creting children view
var PanelItem = Backbone.View.extend({
events: {
"sortStart": "sortEventPropagation"
},
initialize : function (options) {
this.parentView = options.parentView;
},
sortEventPropagation: function(){
this.parentView.trigger('sort:start:propagated', this.model);
},
})
Everytime you are creating panelItem view you need to pass it panel in options as parentView.
var childView = new PanelItem({
parentView: panel
})
And on panel you could easily listenTo sort:start:propagated event
var Panel = Backbone.CollectionView.extend({
initialize: function(){
this.listenTo(this, 'sort:start:propagated', function(model){
//Do magic with model
})
},
})
I am new to javascript and Backbone.js. I would like to bind a custom listener to a Backbone view on initialization. For example, I would like to achieve something like this:
var CampaignListView = Backbone.View.extend({
initialize: function(){
this.on("customFunc")
},
customFunc: function() {
if (this.$el.scrollTop() == 500 ) {
console.log("this has occurred, time to do stuff")
}
}
)}
That whenever a user scrolls to a specified position, I can execute some code.
Thanks.
I'm assuming this is something you want to happen when the window scrolls, no? In that case you have a few options:
The first is something a bit more familiar, and close to what you have, using the remove method to cleanup that binding:
var CampaignListView = Backbone.View.extend({
initialize: function(){
// You probably want to add an identifier to the event name, like 'scroll.campainlist' or something
$(window).on('scroll', _.bind(this.customFunc, this));
},
customFunc: function() {
if (this.$el.scrollTop() == 500 ) {
console.log("this has occurred, time to do stuff")
}
},
remove: function() {
$(window).off('scroll');
Backbone.View.prototype.remove.call(this);
}
)}
The other option is to use an Event Aggregator like so:
var vent = _.extend({}, Backbone.Events);
$(window).on('scroll', function(ev) {
vent.trigger('window:scroll', ev);
});
var CampaignListView = Backbone.View.extend({
initialize: function(){
this.listenTo(vent, 'window:scroll', this.customFunc);
},
customFunc: function() {
if (this.$el.scrollTop() == 500 ) {
console.log("this has occurred, time to do stuff")
}
}
)}
You can make use of delegateEvents in Backbone Views. For example you could attach to the scroll event of your view, but of course it will fire with every scroll. I put together a quick and very simple jsfiddle here based on your example. Below is some of the JavaScript. Notice the use of the events{} in the code.
var CampainListView = Backbone.View.extend({
el: '#list',
events: {
'scroll': 'customFunc'
},
initialize: function() { },
render: function() {
this.$el.html(sampleHtml);
return this;
},
customFunc: function() {
// console.log('scrolling... top is ' + this.$el.scrollTop());
if (this.$el.scrollTop() >= 100 ) {
console.log("this has occurred, time to do stuff")
}
}
});
var view = new CampainListView();
view.render();
I just don't have idea what causes problem and need help. Before posting I've came up to alternative solution, but I want to learn why this is not working properly.
I have router that initialize view which initialize entity collection and views like so:
advertiser_manage_campaign: function () {
this.campaignListView = new window.CampaignListView;
this.mainSidebar = new window.MainSidebar;
},
CampaignListView:
window.CampaignListView = Backbone.View.extend({
el: ("#right_column"),
initialize: function () {
this.render();
this.campaignCollection = new Campaign.CampaignCollection;
this.campaignCollectionView = new Campaign.CampaignCollectionView({ model: this.campaignCollection });
this.campaignCollection.fetch();
},
events: {
"click .campaign_dialog": "openCampaignDialog"
},
openCampaignDialog: function (e) {
var that = this;
var itemID = $(e.target).attr("item-id");
var model = {}; //model to populate dialog inputs
if (!isNaN(itemID))
model = this.campaignCollection.get(itemID).toJSON(); //get existing model from collection <- after described procedure, error
Campaign.Dialog.open(model, function (data) {
if (isNaN(itemID)) {//model does not exist, create
that.campaignCollection.create(data, { wait: true,
error: function (model, error) {
dialoger.showErrors(JSON.parse(error.responseText).errors);
},
success: function (mdl, response) { window.Campaign.Dialog.close(); }
});
} else {//model exist, update
model = that.campaignCollection.get(itemID);
model.save(data, { wait: true,
error: function (mdl, error) {
dialoger.showErrors(JSON.parse(error.responseText).errors);
},
success: function (mdl, response) { window.Campaign.Dialog.close(); }
});
}
});
return false;
},
render: function () {
$(this.el).html(window.Templates.getHTML("campaign_list_view", {}));
$(".button", $(this.el)).button();
}
});
-
openCampaignDialog
is for both edit models and creating new. Every view(table row) of model
has button with class ".campaign_dialog" and there is button for adding new model with same class.
Campaign.Dialog.open
shows dialog populated with model and in callback returns JSON from dialog form.
If I create new model via dialog, I can edit it right away, but when I create new model, change view, back to this view, create again new model, change view and then again back, click edit on last added item, I get error on commented line as model with this ID is not in collection, although it is. Response from server is OK. Obviously, I'm doing something wrong and after one day, I don't see what it is.
Alternative solution I've came up to is to create and populate dialog from event of model view (this works), but I thought that CampaingCollectionView or CampaingView should not deal with adding or editing models so I've implemented this in 'higher' view.
Thanks everyone for helping me...
Edit:
var CampaignCollectionView = Backbone.View.extend({
el: (".content_table tbody"),
initialize: function () {
this.model.bind("reset", this.render, this);
this.model.bind("add", this.add, this);
},
render: function () {
$(this.el).empty();
_.each(this.model.models, function (campaign) {
$(this.el).append(new CampaignView({ model: campaign }).render().el);
}, this);
return this;
},
add: function (model) {
window.Appender.AppendAndScroll($(new CampaignView({ model: model }).render().el), this.el);
}
});
I've found solution.
Problems arise, though, when we bind objects together through these
events but we don’t bother unbinding them. As long as these objects
are bound together, and there is a reference in our app code to at
least one of them, they won’t be cleaned up or garbage collected. The
resulting memory leaks are like the zombies of the movies – hiding in
dark corners, waiting to jump out and eat us for lunch.
Source: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Author suggests unbinding mechanism, but I'm going to reuse same objects if exist.
Router:
advertiser_manage_campaign: function () {
if (!this.campaignListView)
this.campaignListView = new window.CampaignListView;
else
this.campaignListView.initialize();
this.mainSidebar = new window.MainSidebar;
},
If someone thinks this is not best solution, I would like to hear why.
Thank you all who tried to help!