var Router = Backbone.Router.extend({
routes:{
'notes': 'showNotes',
"note/:noteId": "openNote"
},
showNotes: function() {
new app.NotesView;
},
openNote: function(noteId) {
var NotesCollection = new app.NotesCollection();
NotesCollection.fetch();
var view = new app.NotesView({
currentModel : NotesCollection.get(noteId)
})
}
});
Here the problem comes when I navigate to domain.com/#notes every time I navigate there a double view occurs, and any event get's fired multiple times.
I think it's because every time you go there, you create a new view (the old view still exists). Instead, can you just create the view once and on showNotes, you call render?
Also, as a side note, fetch() is an asynchronous call so you have to wait until data is fetched by passing in a callback (success function) and doing your calculations there.
Something like this:
var notesView = new app.NotesView;
var Router = Backbone.Router.extend({
routes:{
'notes': 'showNotes',
"note/:noteId": "openNote"
},
showNotes: function() {
notesView.render(); // or append notesView.$el into the dom somewhere
},
openNote: function(noteId) {
var NotesCollection = new app.NotesCollection();
NotesCollection.fetch({
success: function(){
notesView.setModel(NotesCollection.get(noteId); // make this method youself
}
});
}
});
Related
Having a Backbone model shared between several views is a rather common situation. Nevertheless, let's say this model is a UserModel. It handles several methods, allowing a user to register or login for instance.
And when the user is logged, fetch is called to get the user's data. Therefor the model can't fetch itself with this.fetch() in its initialize method.
Where shoud it be fetched from? How?
This is our simple UserModel:
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
login(email, password, rememberMe, callback) {
…
},
signup(email, password, firstname, lastname, callback) {
…
}
});
Now let's say it's shared by both:
HomeView & CartView
app.HomeView = Backbone.View.extend({
template: app.tpl.home,
initialize() {
// model is passed # instanciation
this.model.fetch();
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
app.CartView = Backbone.View.extend({
template: app.tpl.cart,
initialize() {
// model is passed # instanciation
this.model.fetch();
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
Now if I instanciate HomeView, userModel will be fetched. But if at a later point, I instanciate CartView, this same model will be fetched again. And that makes a useless http request.
Basically, the model could fetch istself after a succesfull call to its login method, but user can arrive on a page or reload his browser being already logged in. Furthermore, a user can land on any page, there's no way to say he's going to HomeView prior to CartView.
There are two options I see. Either UserModel smartly handles multiple fetch calls like so:
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
if (this.fetchTime && hourAgo > this.fetchTime) return true;
return false;
},
fetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
…
});
That way, I'm able to call this.model.fetch() as many times as needed, being stateless in views.
Or, I can handle that on a view layer:
app.HomeView = Backbone.View.extend({
template: app.tpl.home,
initialize() {
// model is passed # instanciation
// fetch model if empty
if (_.isEmpty(this.model.changed)) this.fetch();
// render directly if already populated
else this.render();
// render on model sync
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
If needed, Backbone's model.changed doc reference & Underscore's _.isEmpty's.
Which way is cleaner? Is there any other approach I might have missed?
Personal preference would not be to override fetch but to instead implement a wrapper function, like customFetch
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
if (this.fetchTime && hourAgo > this.fetchTime) return true;
return false;
},
customFetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
…
});
The code example you provided would end up in a loop (this.fetch calling itself...), so my personal preference is to just wrap the core backbone functionality in another function.
I would even go so far as to have my own custom Model that is extended by all the models I use. Eg:
const MyModel = Backbone.Model.extend({
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
return (this.fetchTime && hourAgo > this.fetchTime);
},
customFetch() {
this.fetch();
},
});
Then UserModel would override customFetch and look like this:
const UserModel = MyModel.extend({
customFetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
});
Might not be the best way to do it. For me personally it would be the easy way for it to read and then extend later. I would imagine this customFetch would be used in some/all models so it could be amended as appropriate.
i am facing problem in backbone collection.
this is my router
var LanguageRouter = Backbone.Router.extend({
routes: {
'': 'defaultaction',
'section/:key': 'sectionview',
}
});
collection is
var LanguageCollection = Backbone.Collection.extend({
model: LanguageModel,
url: '/lang'
});
and app.js
var initialize = function () {
var language_router = new LanguageRouter(),
parent_view = new ParentView(),
list_collection = new LanguageCollection(),
list_collection.fetch();
language_router.on('route:defaultaction', function () {
list_view = new LanguageListView({
collection: list_collection,
template: _.template(templates.languagelistsingle)
});
});
Here , after fetching the list_collection i tried passed the collection to language_view but i am getting empty collection only. How to fix this... Thanks in advance
Fetch is not synchronous, so you cannot call it in one line and use the return on the next line. The correct way using backbone is use listenTo or On (depending on the backbone version)
You could change this part:
list_collection = new LanguageCollection();
this.listenTo(list_collection, "sync", this.someFunction());
\\here we are listening the sync event that is automatically fired by backbone when a model or collection is synced with the server.
list_collection.fetch();
someFunction:function(){
//Logic with the collection content...
}
To learn more about events, please take a read here: http://backbonejs.org/#Events-catalog
On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).
Below is my backbone view.
define([
'app',
'backbone',
'twig',
'templates/report',
'data/reportViewCollection',
'data/reportViewModel'
], function (app, Backbone, Twig, template, Collection, Model) {
var collection = new Collection();
var fetching;
return Backbone.View.extend({
setParams: function (rlId, viewId, categoryId, depth) {
// reset any possible pending previous repests.
fetching = false;
var model = collection.getModel(rlId, viewId, categoryId, depth);
if (!model) {
model = new Model({
rlId: rlId,
viewId: viewId,
categoryId: categoryId,
depth: depth
});
fetching = model.fetch({
success: function (model) {
collection.add(model);
},
error: function (model) {
alert('error getting report view');
}
});
}
this.model = model;
},
render: function () {
var that = this;
var done = function() {
app.vent.trigger('domchange:title', that.model.get('title'));
that.$el.html(Twig.render(template, that.model.toJSON()));
that.delegateEvents(that.events);
fetching = false;
};
if (fetching) {
app.loading(this);
fetching.done(done);
} else {
done();
}
return this;
},
events: {
'change select.view-select': 'viewSelect',
'click #dothing': function (e) {e.preventDefault(); alert('hi');}
},
viewSelect: function(e) {
var selectedView = $(e.target).val();
var rlId = this.model.get('rlId');
if (!rlId) rlId = 0;
var url = 'report/' + rlId + '/' + selectedView;
console.log(this, e, url);
Backbone.history.navigate(url, {trigger: true});
}
});
});
Description of functionality:
What happens is when a specific url is navigated to, the setParams() function is called to fetch the model from the server. When the render method is called, it checks if we are currently fetching the model and if so, uses sets a deferred callback to render the template when it gets done fetching. When the model is fetch-ed and we are ready to render, renders the template and fills in the view by that.$el.html().
Problem:
What happens is that my events work perfectly the first time I navigate to a url, but when I hit the back button, my events don't get attached.
I've stepped through the code and can't see any differences. The only real difference is that I'm loading the model from the cached collection immediately instead of doing an ajax request to fetch it.
Any clues what is going on?
try to change:
that.$el.html(Twig.render(template, that.model.toJSON()));
to
that.$el.html("");
that.$el.append(Twig.render(template, that.model.toJSON()));
had kind the same problem and this fixed it.
I resolved my issue. The comment by #mu set me in the right direction.
I am using Marionette and my view is contained in a region. In the Region's open function, it is doing this.$el.html(view.el); which wipes out the events in certain circumstances. I'm still not sure why it does in some but on in others.
The Solution proved to be to add an onShow function to my view that call's this.delegateEvents(). Everything is working smoothly now!
I eventually figured it out by stepping through the code and watching the events registered on the view's div.
I need to pass an id to a collection for use in the url (e.g. /user/1234/projects.json) but am not sure how to do this, an example would be wonderful.
The way my application is structured is on launch a collection of 'users' is pulled and rendered, I then want when a user is clicked their 'documents' are pulled from the server into a new collection and rendered in a new view. The issue is getting the user id into the documents collection to give the relevant URL for the documents.fetch().
think I've got it, here is an example:
//in the the view initialize function
this.collection = new Docs();
this.collection.project_id = this.options.project_id;
this.collection.fetch();
//in the collection
url: function() {
return '/project/api/' +this.project_id+'/docs';
}
Your user collection url should be set to /user. Once that's set, your models should utilize that url in order to do their magic. I believe (not completely positive) that if a model is in a collection, calling the 'url' method will return /user/:id. So all your typical REST-ish functionality will be utilized on '/user/:id'. If you are trying to do something with a relationship (a user has many documents) it's kind of rinse and repeat. So, for your documents collection (which belogs to user correct?) you'd set the url to 'user_instance.url/documents'.
To show a one to many relationship with a backbone model, you'd do something like this (upgrade to backbone 0.5.1 for urlRoot):
var User = Backbone.Model.extend({
initialize: function() {
// note, you are passing the function url. This is important if you are
// creating a new user that's not been sync'd to the server yet. If you
// did something like: {user_url: this.url()} it wouldn't contain the id
// yet... and any sync through docs would fail... even if you sync'd the
// user model!
this.docs = new Docs([], {user_url: this.url});
},
urlRoot: '/user'
});
var Doc = Backbone.Model.extend();
var Docs = Backbone.Collection.extend({
initialize: function(models, args) {
this.url = function() { args.user_url() + '/documents'; };
}
});
var user = new User([{id: 1234}]);
user.docs.fetch({ success: function() { alert('win') });
Why do you need to override the URL property of the collection with a function?.. you could do:
this.collection = new Docs();
this.collection.project_id = this.options.project_id;
this.collection.url = '/project/api/' + this.options.project_id + '/docs';
this.collection.fetch();
I like the answer from Craig Monson, but to get it working I needed to fix two things:
Binding the User url method before passing it to the Docs
A return statement from the url function in Docs
Updated example:
var User = Backbone.Model.extend({
initialize: function() {
// note, you are passing the function url. This is important if you are
// creating a new user that's not been sync'd to the server yet. If you
// did something like: {user_url: this.url()} it wouldn't contain the id
// yet... and any sync through docs would fail... even if you sync'd the
// user model!
this.docs = new Docs([], { user_url: this.url.bind(this) });
},
urlRoot: '/user'
});
var Doc = Backbone.Model.extend();
var Docs = Backbone.Collection.extend({
initialize: function(models, args) {
this.url = function() { return args.user_url() + '/documents'; };
}
});
var user = new User([{id: 1234}]);
user.docs.fetch({ success: function() { alert('win') });