Scenario
I am working on backbone app. What is happening right now is when user clicks edit link on page then it should show a form. I am trying to implement this using backbone routers rather than events. With events object it works perfectly fine. To use routers, I am using global events.
Problem
The problem is that when user clicks on edit link, it shows me following error in console
Uncaught TypeError: Object 10 has no method 'toJSON' views.js:57
This error is because on line 57 in views.js, I am using this.model.toJSON() whereas I am not passing model via router. I don't know how pass model through router
Here is my router. Note: All of the following codes are in separate files
App.Router = Backbone.Router.extend({
routes: {
'contacts/:id/edit': 'editContact'
},
editContact: function (id) {
console.log('yahhhhh');
vent.trigger('contact:edit', id);
}
});
In above router I am triggering an event inside editContact function. Then I am listening to above event in following initialize function.
App.Views.App = Backbone.View.extend({
initialize: function () {
vent.on('contact:edit', this.editContact, this);
},
editContact: function (contact) {
var editContactView = new App.Views.EditContact({ model: contact });
$('#editContact').html(editContactView.render().el);
}
});
Now in above after listening to event in initialize function, I am calling editContact function and I am also passing model using this keyword. Inside editContact function, I am creating an instance of EditContact, view which is following, and then rendering a form which needs to be shown.
App.Views.EditContact = Backbone.View.extend({
template: template('editContactTemplate'),
render: function () {
var html = this.template(this.model.toJSON()); //<--- this is line 57
this.$el.html(html);
return this;
}
});
After doing all of the above, the form is not shown and I am getting above mentioned error.
Question
How do I pass model to render function inside EditContact via router so that it starts working?
UPDATE
Here is my model
App.Models.Contact = Backbone.Model.extend({
urlRoot : '/contacts'
});
In your editContact method the argument contact refers to the id you pass onwards from the router. When you initialize a new view with new App.Views.EditContact({ model: contact }) the model of the view will, expectedly, be the id.
You need to map the id into a model instance. IMHO the correct place to do this is in the router:
editContact: function (id) {
var contact = new App.Models.Contact({id:id});
vent.trigger('contact:edit', contact);
}
Notice that at this point the model will only have the id property set. If you need to populate the model properties for editing, you should fetch the model from the server, and only then trigger the event:
editContact: function (id) {
var contact = new App.Models.Contact({id:id});
contact.fetch().done(function() {
vent.trigger('contact:edit', contact);
});
}
Edit based on comments: Generally speaking you shouldn't pass anything to the router. The router should be a starting point for every new request (url change). If you want to hold some state between page changes, you should store the data on the router level, and pass the models and collections "down" from the view.
In a simplified scenario this would mean initializing and storing a reference to the collection in the router. Something like:
var Router = Backbone.Router.extend({
initialize: function() {
this.contactCollection = new App.Collections.Contacts();
},
editContact: function (id) {
id = parseInt(id, 10);
if(_.isNaN(id)) {
//error...
}
//try to get a model from the collection
var contact = this.contactCollection.get(id);
//if not found, create, add and fetch
if(!contact) {
contact = new App.Models.Contact({id:id});
this.contactCollection.add(contact);
contact.fetch().done(function() {
vent.trigger('contact:edit', contact);
});
} else {
vent.trigger('contact:edit', contact);
}
}
});
Please note that this is just example code, and not necessarily how you should implement it, line by line. You should consider whether it's OK to display a potentially stale model in the view, or whether you should always fetch it from the server. In practice you might also abstract the collection state in a nice class, instead of handling it directly in the router.
Hope this answers your questions.
Related
I am fairly new to Ember, being a hardcore backbone aficionado for many years and I'm at a loss for how to handle a situation I've run into.
I am using Pusher and the Pusher Ember library to build a sort of chat application.
The way it works is, a person navigates to a users account page and it creates a new "conversation". Then, once that conversation has been created, I would like to subscribe to a pusher channel that is dynamically named after that conversations id. I need to define the Pusher Subscriptions on my controller.
Here's my route (in coffeescript)
App.ConversationShowRoute = Ember.Route.extend
model: (params) ->
#store.createRecord('conversation', user_id: params.user_id).save()
and my controller:
App.ConversationShowController = Ember.ObjectController.extend
init: ->
subscriptions = []
subscriptions[#model.get('id')] = ['newMessage']
PUSHER_SUBSCRIPTIONS: subscriptions
Unfortunately, the model has not resolved at this point so I don't yet know what my #model.id is, and it fails.
Any advice for the best way to handle this?
I had the same issue when I added ember-pusher to my application. The solution I went with is to define a variable in the App namespace that could be referenced. (Not ideal and something I'll need to fix later)
init: function () {
this.channelName = 'presence-document-' + App.documentId + '-channel';
this.PUSHER_SUBSCRIPTIONS[ this.channelName ] = [
'pusher:subscription_succeeded', 'pusher:member_added', 'pusher:member_removed',
'client-send-status', 'client-send-message'
];
this._super();
}
A second, cleaner option, would be to try a needs relationship with your user controller, but I'm not sure if that will be available until after init is complete.
App.ConversationShowController = Ember.ObjectController.extend({
needs: ['user'],
userId: Ember.computed.alias('controllers.user.id'),
init: function() {
this.PUSHER_SUBSCRIPTIONS[this.get('userId')] = ['newMessage'];
this._super(); // Maybe try putting this first if properties haven't resolved.
}
});
A third option would be to lookup the user controller (singleton) during init.
App.ConversationShowController = Ember.ObjectController.extend({
init: function() {
var userController = this.container.lookup('controller:user');
this.PUSHER_SUBSCRIPTIONS[userController.get('id')] = ['newMessage'];
this._super();
}
});
UPDATE
Since you need the conversation id you can observe when the model changes and wire up pusher the way ember-pusher/bindings.js does. You would no longer need to override controller.init, just set PUSHER_SUBSCRIPTIONS: {} to start with.
afterModelLoad: function() {
this.channelName = 'conversation-' + this.get('model.id');
this.PUSHER_SUBSCRIPTIONS[this.channelName] = ['newMessage'];
// From bindings.js init
var events = this.PUSHER_SUBSCRIPTIONS[this.channelName];
this.pusher.wire(this, this.channelName, events);
this.removeObserver('afterModelLoad'); /* If you only want to run once */
}.observes('model')
I need to work with backbone.js, i can't go to "render" part inside my view here is my code:
var Vigne = {
Models:{},
Collections: {},
Views:{},
Templates:{}
}
Vigne.Models.Raisin = Backbone.Model.extend({})
Vigne.Collections.Grape = Backbone.Collection.extend({
model: Vigne.Models.Raisin,
url: "./scripts/data/vin.json",
initialize: function (){
console.log("grape initialised");
}
});
Vigne.Views.Grape= Backbone.View.extend({
initialize: function(){
_.bindAll(this,"render");
this.collection.bind("reset",this.render);
},
render: function(){
console.log("render");
console.log(this.collection.length);
}
})
Vigne.Router = Backbone.Router.extend({
routes:{
"": "defaultRoute"
},
defaultRoute: function(){
console.log("defaultRoute");
Vigne.grape = new Vigne.Collections.Grape()
new Vigne.Views.Grape ({ collection : Vigne.grape });
Vigne.grape.fetch();
console.log(Vigne.grape.length);
}
}
);
var appRouter= new Vigne.Router();
Backbone.history.start();
I am expecting it to display my collection's length in the debugger's console, it seem's like it doesn't reset. Any ideas?
Edit:
i added this within the fetch function:
success: function(){
console.log(arguments);
},
error: function() {
console.log(arguments);
}
});
and the fetch function succeed on getting the json file, but it doesn't trigger the reset function.
i solved this problem by setting the attribute within the fetch function to true:
Vigne.grape.fetch({
reset:true,
error: function() {
console.log(arguments);
}
}
);
This book helped me : http://addyosmani.github.io/backbone-fundamentals/
Backbone calls reset() on fetch success which in turns triggers reset event. But If your fetch fails due to some reason, you won't get any event. So you have to pass an error handler in fetch method and use it to identify the error and handle it.
Vigne.grape.fetch({
error: function() {
console.log(arguments);
}
});
You can also pass success call back and you will be able to know the problem in your fetch.
You can also use Charles proxy/Chrome Debuuger Tool to identify if you are getting proper response from your backend.
Can you please paste your response what you are getting from server. You may vary the data but just keep the format right.
Edit:
One more problem I can see is that you have not defined attributes in your model So after Backbone fetch, it refreshes your collection with the new models fetched from server. Fetch method expects an array of json objects from server and each json object in response array should match with the attributes you have set in defaults in your model Otherwise it won't be able to create new models and hence won't be able to refresh your collection. Can you please rectify this and let me know.
The workflow is:
User is on the New Page.
Hits save, causing model validations to fail. Errors bound to the model are shown. Still on the same page.
The user now navigates to the Index page and sees the invalid record added to the list.
The ArrayController seems to be adding records which failed validations.
App.CompaniesNewRoute = Ember.Route.extend({
model: function(){
var company = App.Company.createRecord();
this.wireObservers(company, this);
return company;
},
events: {
save: function(){
var controller = this.controllerFor(this.routeName);
controller.get('transaction').commit();
}
},
wireObservers: function(company, router) {
company.on('becameInvalid', function(record){
// do something to remove it from the arraycontroller
// record.rollback();
});
company.on('didCreate', function(){
router.transitionTo('companies.index')
});
})
})
The becameInvalid event does get called. Doing a record.rollback() throws an exception:
Uncaught Error: Attempted to handle event `becameClean` on <App.Company:ember612:null> while in state rootState.loaded.created.invalid. Called with undefined ember-data.js:3495
DS.StateManager.Ember.StateManager.extend.unhandledEvent ember-data.js:3495
Is there a way to prevent ArrayController to add records which failed validation.
Try to rollback the transaction through the store instead.
wireObservers: function(company, router) {
var _self = this;
company.on('becameInvalid', function(record){
// do something to remove it from the arraycontroller
_self.store.rollback();
});
company.on('didCreate', function(){
router.transitionTo('companies.index')
});
})
You should consider creating a specific transaction for the purpose rather than using the default one. To create a new transaction inside a route, you can do the following
App.MyRoute = Ember.Route.extend({
transaction: this.store.transaction();
})
and then create your record and add it to the transaction using
var company = this.transaction.createRecord(App.Company);
and finally commit or rollback the transaction
this.transaction.commit();
this.transaction.rollback();
In your case, I think a rollback() is not the solution, because the transaction has already been committed ; even if the server validation fails, the record has been added to the ArrayController content.
The solution I could see would be to use the deleteRecord function, with something like this :
wireObservers: function(company, router) {
company.on('becameInvalid', function(record){
record.deleteRecord();
});
company.on('didCreate', function(){
router.transitionTo('companies.index');
});
})
I've just a question about your workflow. When you get an validation error, and leave the page, it means the user cancelled the record creation
I'm new to backbone.js and trying to understand how routes, views etc works and now I have a problem with events building up for the same view. here is a clip that will show you exactly what I mean. http://screencast.com/t/QIGNpeT2OUWu
This is how my backbone router looks like
var Router = Backbone.Router.extend({
routes: {
"pages": "pages",
}
pages: function () {
var page_view = new PageView();
}
});
So when I click the Pages link I create a new PageView and this is the code I'm using
PageView = Backbone.View.extend({
el: $("#content"),
initialize: function () {
$.ajax({
url: '/pages',
success: function (data) {
$("#content").html(data);
}
});
},
events: {
"click td input[type=checkbox]": "updatePublishedStatus"
},
updatePublishedStatus: function (event) {
console.log('update publish status');
}
});
pretty basic I guess but as you can see in the clip each time I navigate to /pages I get another event registered to the checkbox.
There are a few things going wrong here.
Your video indicates pages being a collection well, Pages. Pages being a Backbone.Model with attributes such as Page name, slug, published etc... You lack that and it's going to hurt. You shouldn't just load some html and push it to your DOM, this defies the whole purpose of using Backbone in the first place.
If you do create a Model for a Page it will have a View. Then your /pages route will show the view of the Collection Pages etc.
You will fetch your data not inside a view's initialize but rather by doing pages.fetch(); where pages is an instance of the Pages collection. This can happen before you even initialize your router.
When changing attributes through your view, the individual models will be updated.
As a sidepoint: Fetching data on initialize is not great. You can call render() before you actually get the data and that's no fun.
Also instead of doing $('#content') you can use the view's $el. As in this.$el.html(...);
Move var page_view = new PageView() to be outside of Router.pages().
Have the PageView.initialize() success callback save data to a variable. Either in PageView or in a model.
Add a render function to PageView that sets $("#content").html(data);.
Call page_view.render() within Router.pages().
I have a controller property called authenticated which defaults to false. However, in my login view I need to be able to set it to true. Also, in my logout view I need to be able to set it to false. How can I expose this property within the view?
var Controller = Backbone.Controller.extend({
...
authenticated: false,
login: function() {
if(this.authenticated)
{
location.hash = '!/dashboard';
} else {
new LoginView();
}
},
logout: function() {
$.post('/admin/logout', {},
function(resp){
}, "json");
this.authenticated = false;
location.hash = '!/login';
}
...
});
Your controller is correctly doing the login and logout functionality. All you need to do is have your view fire backbone.js events and have the controller be registered to receive those.
Somewhere in your controller, you need something like:
var loginView = new LoginView(...); // params as needed
loginView.bind("login_view:login", this.login);
loginView.bind("login_view:logout", this.logout);
loginView.render();
Also, you need to assure that the controller is set up to handle events, so something like this is needed in your initialize function:
_.extend(this, Backbone.Events);
_.bindAll(this, "login", "logout");
Your view will need the event code, so be sure to add the _.extend(...) call into its initialize.
In your view where appropriate, you need:
this.trigger("login_view:login");
and
this.trigger("login_view:logout");
As a final note, you want the controller to do the login and logout server calls. All you need from the view is an event and potentially a populated model or data otherwise. This data would be passed as a parameter in the trigger statement(s) and would be received as an argument in the login/logout functions. I have not included this in the code, however.
You basically want the view to manage the DOM and bubble up application events to the controller. The controller can then mediate with the server and manage any necessary views.