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.
Related
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);
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;
}
});
I am running the following view:
app.OrganisationTab = Backbone.View.extend({
el : "#organisations",
template : _.template( $("#tpl-groups-list").html() ),
events : {
"click .js-edit-group" : "editGroup"
},
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
var that = this;
this.collection.fetch({
success: function() {
that.render();
}
})
},
change: function() {
//this.$el.empty();
console.log("collection has changed");
},
render:function() {
this.$el.empty();
this.addAll();
return this;
},
addAll: function() {
this.collection.each(this.addOne, this);
},
addOne: function(model) {
var view = new app.GroupEntry({
model: model
});
this.$el.append(view.render().el);
},
editGroup: function(e) {
e.preventDefault();
var elm = $(e.currentTarget),
that = this;
$('#myModal').on('hidden.bs.modal', function () {
$('.modal-body').remove();
});
var organisation = this.collection.findWhere({ id : String(elm.data('groupid')) });
var members = organisation.get('users');
organisation.set('members', new app.UserCollection(members));
var projects = organisation.get('projects');
organisation.set('projects', new ProjectCollection(projects));
var orgForm = new app.createOrganisationForm({
model : organisation,
});
$('#myModal').modal({
backdrop: 'static',
keyboard: false
});
}
});
This view triggers a new view, and in that I can change a model save it (sends a PUT) and I can get in my console, collection has changed. If I console.log this collection I can see that the collection has changed. If I try and re-render the page all I see are the models as they were without the edits.
Why would this be happening, when clearly the collection is getting changes as it fires the events and I can see it when I log the collection?
After reading your comment:
No sorry on collection change I try to run render() which should empty
the container, and add all the models...but it seems to render the old
collection again.
You're getting this problem because you are overriding the success handler for the fetch call. That success callback is triggered before the models are placed in the collection. You need to listen to the sync event if you want render after the collection has been synchronized with the server (models are updated after fetch).
Update initialize to:
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
this.listenTo(this.collection, 'sync', this.render);
this.collection.fetch();
},
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": []
}
});
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.