Backbone.js multiple views, one collection, one fetch - javascript

I am trying to generate multiple views using one collection and one fetch every 5 seconds.
Below is a working example, but both views are refreshed when fetched.
I could splice the response into multiple urls, but i want to minimize the aumount of requests.
My current problem is that i dont want all views to re-render every 5 seconds when the collection is re-fetched, only the associated view that changed.
I have tried creating multiple models inside the collection and adding the correct object in the parse function without any luck.
Response:
{
"json-1": {
"sub_1": "3",
"sub_2": [],
},
"json-2": {
"sub_1": [],
"sub_2": "1",
},
}
// Client
const APICollection = Backbone.Collection.extend({
initialize: (models, options) => {
this.id = options.id;
},
url: () => {
return 'https://url.url/' + this.id;
},
model: APIModel,
parse: (resp) => {
return resp;
},
});
const ViewOne = Backbone.View.extend({
initialize: function () {
this.collection.bind('sync', this.render, this);
this.update();
_.bindAll(this, 'update');
},
render: function (n, collection) {
// Render view
},
update: function () {
let self = this;
this.collection.fetch({
update: true, remove: false, success: function () {
setTimeout(self.update, 5000);
}
});
}
});
// Also updates when re-fetched
const ViewTwo = Backbone.View.extend({
initialize: function () {
this.collection.bind('sync', this.render, this);
},
render: function (n, collection) {
// Render function
}
});
let col = APICollection([], {id: 'someid'});
new ViewOne({collection: col, el: $("#one")});
new ViewTwo({collection: col, el: $("#two")});
**Update
To clarify: "only the associated view that changed". By this i mean that 'ViewOne' should only be re-rendered when 'json-1' has changed, and 'ViewTwo' shouldn't re-render. currently the full response is sent to both views.

When dealing with an API which returns an Object, not an array of Objects, the best approach is to use a Backbone.Model directly.
update: function () {
let self = this;
this.model.fetch({
update: true, remove: false, success: function () {
setTimeout(self.update, 5000);
}
});
}
The model is still fetched the same way as the collection, but the Views can listen to specific attributes on the model, instead of:
this.collection.bind('sync', this.render, this);
The following can be used:
this.model.bind('change:json-1', this.render, this);
Tip: Better to listenTo rather than bind, it is safer (see docs)
this.listenTo(this.model, 'change:json-1', this.render);

Related

Rendering the view returns undefined

I've got a collection view with two filter methods, and a render method which takes a parameter. The problem I'm stuck with is that when rendering the view for the first time it returns me an error. Here's my collection:
var ResumeCollection = Backbone.Collection.extend({
url: 'http://localhost:3000',
filterActive: function () {
var active = this.where({interviewed: false});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return new ResumeCollection(interviewed);
}
});
And my view:
var ResumeList = Backbone.View.extend({
events { // hash array of filter events },
initialize: function () {
this.collection.fetch();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
showActive: function (ev) {
var filtered = this.collection.filterActive();
this.render(filtered);
},
showInterviewed: function (ev) {
var filtered = this.collection.filterInterviewed();
this.render(filtered);
},
showAll: function (ev) {
this.render(this.collection);
}
});
This view gets rendered for the first time in my router by passing a collection:
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function () {
this.layout = new LayoutView();
}
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
});
And this is the layout view within which all the other views are rendered:
var LayoutView = Backbone.View.extend({
el: $('#outlet'),
render: function (view) {
if (this.child && this.child !== view) {
this.child.undelegateEvents();
}
this.child = view;
this.child.setElement(this.$el).render();
return this;
}
});
When I just refresh my page, I get filtered.toArray is not a function error and nothing is rendered respectively. After inspecting everything in the debugger, I found out that when the view gets rendered for the first time, the filtered attribute receives an empty collection, assigns it to data variable, which becomes an empty array and goes to the body of render function, becoming undefined after that. The mysteries go here: whenever I click items, that are bound to my show* events, they act exactly as expected and render either models where interviewed === false, or true or the whole collection. This looks kinda magic to me and I haven't got the faintest idea what can I do with that.
ADDED: GitHub repo with this project
Your home function on the AppRouter has a typo. You have an extra semi-colon.
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection();
}));
}
Should be
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
I needed to remove it to get the JSFiddle working: https://jsfiddle.net/4gyne5ev/1/
I'd recommend adding some kind of linting tool into your IDE or Build process (http://eslint.org/)
You need to add home url content to your db.json file like this
"" : [
{
'somthing': 'somthing'
}
]
After a piece of advice from my mentor I realized that the core of the problem was in asynchronous origin of fetch method -- as I passed this.collection.fetch in my initialize function, it executed after my render method, not before it, so my render method had just nothing to render when the view was called for the first time. So, this fix worked:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection();
// removed .fetch() method from here
},
render: function (filtered) {
var self = this;
var data;
// and added it here:
this.collection.fetch({
success: function (collection) {
if (!filtered) {
data = collection.toArray();
} else {
data = filtered.toArray();
}
self.$el.html(self.template(collection.toJSON()));
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
})
}
});
}
});
And this worked perfectly and exactly as I needed.

Wrong backbone collection length. Can't each this collection

Sorry for my bad English. Tell me why the following happens:
I have some backbone collection:
var Background = window.Models.Background = Backbone.Model.extend({});
var Backgrounds = window.Models.Backgrounds = Backbone.Collection.extend({
model: window.Models.Background,
url: '/backgrounds/',
initialize: function() {
this.fetch({
success: this.fetchSuccess(this),
error: this.fetchError
});
},
fetchSuccess: function( collect_model ) {
new BackgroundsView ({ collection : collect_model });
},
fetchError: function() {
throw new Error("Error fetching backgrounds");
}
});
And some view:
var BackgroundsView = window.Views.BackgroundsView = Backbone.View.extend({
tagName: 'div',
className: 'hor_slider',
initialize: function() {
this.render();
},
render: function() {
console.log(this.collection);
this.collection.each( function (background) {
console.log(background);
//var backgroundView = new BackgroundView ({ model: background });
//this.$el.append(backgroundView.render().el);
});
}
});
now i creating collection
var backgrounds = new Models.Backgrounds();
but when I must render this view, in the process of sorting the collection its length is 0, but should be two. This log I see at console. How is this possible? What am I doing wrong??
You are creating the view before the collection fetch is successfull. Your code should be:
initialize: function() {
this.fetch({
success: this.fetchSuccess,
//------------------------^ do not invoke manually
error: this.fetchError
});
},
fetchSuccess: function(collection, response) {
new BackgroundsView ({ collection : collection});
},
You should let backbone call fetchSuccess when the fetch succeeds. Right now you're invoking the funcion immediately and passing the return value undefined as success callback.
This looks like a wrong pattern. Your data models shouldn't be aware of/controlling the presentation logic.
You have a view floating around without any reference to it. You should be creating a view instance with reference(for example from a router, or whatever is kick starting your application) and passing the collection to it. Then fetch the collection from it's initialize method and render after the fetch succeeds. Collection can be referenced via this.collection inside view.
Alternatively you can fetch the collection from router itself and then create view instance. Either way collection/model shouldn't be controlling views.
If the code is structured in the following way, the problem is solved. It was necessary to add a parameter reset to fetch.
var Background = window.Models.Background = Backbone.Model.extend({});
var Backgrounds = window.Models.Backgrounds = Backbone.Collection.extend({
model: window.Models.Background,
url: '/backgrounds/',
initialize: function() {
this.fetch({
reset : true,
});
}
});
var BackgroundsView = window.Views.BackgroundsView = Backbone.View.extend({
tagName: 'div',
className: 'hor_slider',
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.collection.each( function (background) {
var backgroundView = new BackgroundView ({ model: background });
this.$el.append(backgroundView.render().el);
}, this);
$('#view_list').empty();
$('#view_list').append(this.$el);
return this;
}
});

BackboneJS Hide Model when date has expired

I have a Backbone App where I display a Collection of Models based on JSON data. Inside the JSON data, I have endDate-which gives me the realtime date. It is based on a competition module. What I want to achieve is that if the given date has expired I want to hide (or even maybe remove) the Model from the collection, so that the competition is no longer available.
So far my competition.js, with the Model in the bottom looks like this:
Competition.View = Backbone.View.extend({
tagName: 'ul',
template: 'competition',
initialize: function() {
this.listenTo(this.model, 'sync', this.render);
},
serialize: function() {
return this.model.toJSON();
}
});
Competition.CompetitionModel = Backbone.Model.extend({
url: function() {
return App.APIO + '/i/contests';
},
comparator: function(item) {
return item.get('endDate');
},
defaults: {
"data": []
}
});
Then in my main module, I import the competition.js, and here I fetch the model and render it in specific HTML element (dont know if its necessary to copy/paste it here for my original question):
function (App, Backbone, Competition) {
var CompetitionOverview = App.module();
CompetitionOverview.View = Backbone.View.extend({
template: 'competitionOverview',
initialize: function(){
this.render();
},
beforeRender: function() {
var competitionModel = new Competition.CompetitionModel();
this.insertView('.singleComp', new Competition.View({model: competitionModel}));
competitionModel.fetch();
},
});
return CompetitionOverview;
}
So, how can I achieve to hide/remove the Models which dates have expired?
thanks in advance...
You state that you have Collection of Models, but your Competition.CompetitionModel extends Backbone.Model instead of Backbone.Collection. In my answer I assume that CompetitionModel is a Backbone.Collection and not a Backbone.Model.
That said, I think you have two options:
Check in your render function of Competition.View whether you should actually show something based on the end-date:
Competition.View = Backbone.View.extend({
tagName: 'ul',
template: 'competition',
initialize: function() {
this.listenTo(this.model, 'sync', this.render);
},
serialize: function() {
return this.model.toJSON();
},
render: function(){
//dependending on in which format your date is you might have to convert first.
if(this.model.get('endDate') < new Date().getTime()){
//render the view
}
});
Or, and I think this is more clean, you check the date as soon as the data comes in from the server. I think backbone triggers an "add" event on the collection when the collection is fetched from the server. So, again, make your Competition Model a Competition Collection and listen to the add event. Change
Competition.CompetitionModel = Backbone.Collection.extend({
initialize: function () {
this.on("add", checkDate)
},
checkDate: function(model, collection, options){
//again, get the right conversion
//but here you should compare your end date with the expire date
if(model.get('endDate') < new Date().getTime()){
this.remove(model.id);
}
},
url: function () {
return App.APIO + '/i/contests';
},
comparator: function (item) {
return item.get('endDate');
},
defaults: {
"data": []
}
});

How do I fill my view with my new backbone collection data after a reset?

I have a model called TimesheetRow and a collection called TimesheetRows.
If I make a call to TimesheetRows.reset(newCollection); where newCollection is a collection of four TimesheetRow models in JSON format, four new model views appear on my page.
How can I fill these input and select fields with the values from the new collection? I've noticed that if I change a field on one of the four models, the collection itself is properly updated, it seems like a one-way bind (does that make sense?).
Here is some code, I apologize for the quantity.
var TimesheetRow = Backbone.Model.extend({
defaults: function () {
return {
MondayHours: 0,
TuesdayHours: 0,
WednesdayHours: 0,
ThursdayHours: 0,
FridayHours: 0,
SaturdayHours: 0,
SundayHours: 0,
JobNo_: null,
PhaseCode: null,
TaskCode: null,
StepCode: null,
WorkTypeCode: null,
Description: "",
};
},
clear: function () {
this.destroy();
}
});
var TimesheetRowList = Backbone.Collection.extend({
model: TimesheetRow,
});
var TimesheetRows = new TimesheetRowList;
var TimesheetRowView = Backbone.View.extend({
template: _.template($('script.timesheetTemplate').html()),
events: {,
"change input" : "changed",
"change select" : "changed"
},
render: function () {
Backbone.Model.bind(this);
this.$el.html(this.template(this.model.toJSON())).hide().slideDown();
return this;
},
initialize: function () {
_.bindAll(this, "changed");
//this.model.bind('change', this.render, this);
//this.model.bind('reset', this.render, this);
this.model.bind('destroy', this.remove, this);
},
changed: function (evt) {
var changed = evt.currentTarget;
var value = this.$("#"+changed.id).val();
var obj = "{\""+changed.id.replace("Timesheet", "") +"\":\""+value+"\"}";
var objInst = JSON.parse(obj);
this.model.set(objInst);
},
});
var TimesheetRowsFullView = Backbone.View.extend({
el: $("#timesheet-rows-container-view"),
events: {
"click #add-new-timesheet-row" : "createRow",
"click #submit-new-timesheet" : "submitTimesheet",
"click #request-sample-timesheet" : "requestTimesheet"
},
initialize: function () {
TimesheetRows.bind('add', this.addOne, this);
TimesheetRows.bind('reset', this.addAll, this);
TimesheetRows.bind('all', this.render, this);
},
addOne: function (timesheetrow) {
var view = new TimesheetRowView({ model: timesheetrow });
this.$("#timesheet-start-placeholder").prepend(view.render().el);
},
addAll: function () {
TimesheetRows.each(this.addOne);
},
render: function () {
//Is this the code I'm missing?
},
createRow: function () {
TimesheetRows.create();
},
requestTimesheet: function () {
//newCollection is retrieved from database in code that I've excluded.
TimesheetRows.reset(newCollection);
}
});
var TimesheetRowsFullViewVar = new TimesheetRowsFullView();
In my changed function, I include Timesheet prefix because my IDs on those fields are all prefixed with Timesheet.
I know for a fact that my new JSON collection object is correctly formatted.
The two lines that are commented out in the TimesheetRowView initialize function were giving me trouble when I would update fields. I'm not sure if they are required or not.
Questions:
When I "reset", my previous model views are still present, how would I get rid of them? Should I use JQuery?
I expect at some point I need to add the Timesheet prefix back on, for the models to find the right IDs. At which step or in which function do I do this? (This ties in to the "one-way bind" I mentioned.)
I've been following along slightly with the TODO application, and I didn't find any of the code in the render function to be necessary for my side, in the FullView. Is this where I'm missing code?
collection.reset() only removes the models from your collection and put other collection in there if you pass them as a param.
To have your views removed from DOM you should do it yourself.
A way to do it is to listen to the reset event it triggers when all is done and create a method that removes your views from the DOM.
Or
Just remove then in that requestTimesheet method of yours.

Backbone.js model event not triggering

I've got the following view file:
var BucketTransferView = Backbone.View.extend(
{
initialize: function(args)
{
_.bindAll(this);
this.from_bucket = args.from_bucket;
this.to_bucket = args.to_bucket;
},
events:
{
'click input[type="submit"]' : 'handleSubmit',
},
render: function()
{
$(this.el).html(ich.template_transfer_bucket(this.model.toJSON()));
return this;
},
handleSubmit: function(e)
{
that = this;
this.model.save(
{
date: 1234567890,
amount: this.$('#amount').val(),
from_bucket_id: this.from_bucket.get('id'),
to_bucket_id: this.to_bucket.get('id')
},
{
success: function()
{
// recalculate all bucket balances
window.app.model.buckets.trigger(
'refresh',
[that.to_bucket.get('id'), that.from_bucket.get('id')]
);
}
}
);
$.colorbox.close();
}
});
My buckets collection has this refresh method:
refresh: function(buckets)
{
that = this;
_.each(buckets, function(bucket)
{
that.get(bucket).fetch();
});
}
My problem is that when the fetch() happens and changes the collection's models, it's not triggering change events in other view classes that has the same models in it. The view's models have the same cid, so I thought it would trigger.
What's the reason this doesn't happen?
Fetch will create new model objects. Any view that's tied to the collection should bind to the collection's reset event and re-render itself. The view's models will still have the same cid's because they're holding a reference to an older version of the model. If you look at the buckets collection it probably has different cids.
My suggestion is in the view that renders the buckets, you should render all the child views and keep a reference to those views. then on the reset event, remove all the child views and re-render them.
initialize: function()
{
this.collection.bind('reset', this.render);
this._childViews = [];
},
render: function()
{
_(this._childViews).each(function(viewToRemove){
view.remove();
}, this);
this.collection.each(function(model){
var childView = new ChildView({
model: model
});
this._childViews.push(childView);
}, this)
}
I hope this works for you, or at least gets you going in the right direction.

Categories

Resources