I have a UserPanel view, which uses a UserModel as its model. In the template for the UserPanel, I have a conditional that checks whether or not the model is undefined. If the model exists, it displays user information. If it doesn't, it displays the "registration" form.
On the user information part of the UserPanel, I have what's essentially an "unregister" button. A user clicks it, and the user information is deleted. The UserPanel responds by re-rendering, allowing them to register a different UserModel.
Common sense tells me to call this.model.destroy. When I use this method, my model is deleted from my data store, but the object still exists in this.model. When the view responds to the model update (by calling render), it still thinks it has a valid model, with all its data and the like. I can call delete on this.model, but that doesn't trigger any events. I can't trigger the event before I delete, because then the view updates before I can delete the model. I have setup the view to respond to model deletions with a method that simply uses delete on the model, then calls render. This works, but bothers me on a conceptual level, since those methods are for handling view updates and not more model manipulation.
In general, what's the proper way to explicitly dispose of a model that is not stored by a collection?
EDIT: I am using Backbone.localStorage as my data store. This might have something to do with it.
Instead of binding to the destroy event I would use the success callback of the model.destroy like so:
this.model.destroy({ success: _.bind(this.onModelDestroySuccess, this) });
Then rename your modelDestroyedView to onModelDestroySuccess:
onModelDestroySuccess: function () {
delete this.model;
this.render();
},
I would also define a cleanupModelEvents method that cleans up your event bindings to the model:
cleanupModelEvents: function() {
this.stopListening(this.model);
},
And call that from onModelDestroySuccess:
onModelDestroySucess: function () {
this.cleanupModelEvents();
delete this.model;
this.render();
},
Hope this helps.
this.model.destroy();
and then on the destroy event
this.model = null;
Related
The best way to think of the problem is to consider it in terms of the Backbone TodoMVC app (even though that's not what I'm doing, it's similar in that it has one main view and many list views). If you declare a custom destroy method on the Todo model (such as one I have below) and click on a view to delete it, then in the method that's triggered (where this.model.destroy is called), you can't invoke the custom destroy method on the model, because this.model is not an instance of the model. Somehow calling this.model.destroy
clear: function () { //in the todo app, this method is called clear
this.model.destroy();
}
works to destroy the model but not if you're trying to invoke a custom destroy method.
Below, I explain the same in terms of my app.
I have a Backbone application that uses server side storage but am unable to send a delete request to the server. Using another SO answer, I created a custom destroy method on the model (FunkyModel shown below) and I try to call that custom destroy method in a deleteModel method in the view but to no avail.
However, it's not calling the custom destroy method on FunkyModel.When I inspect this.model, it doesn't say it's an instance of FunkyModel, rather it only says Backbone.Model and then lists the model's properties. So obviously it's not an instance of FunkyModel and therefore not a surprise that it can't call the custom destroy method on FunkyModel, but at the same time under the attributes property, it has all the attributes of an instance of FunkyModel.
Question: how can a model have attributes that are unique to an instance of a model but not be an instance of that model, but rather simply Backbone.Model
Further Info:
The application is structured like the BackboneMVC Todo App in that there is one main view and then it attaches more views as list items. When I click on a delete symbol on one of the views, it triggers an event that calls the deleteModel method, in which I call
deleteModel(e){
this.model.destroy();
}
So naturally, depending on which list view I click on, the attributes of this.model will be different, but in no case is it ever an instance of the model. It's simply a Backbone.Model and its attributes have the properties of a particular instance. I'm not sure how a model that's not an instance can have properties in its attributes that are from a particular instance.
model
export class FunkyModel extends Model{
defaults(){
return {
name: '',
type: ''
}
}
addToCollection(){
collection.add(this);
this.save();
}
destroy(options){
var opts = _.extend({url: '/api/'}, options || {});
console.log("never getting run");
return Backbone.Model.prototype.destroy.call(this, opts);
}
}
view
deleteModel(e){
this.model.destroy({success: function(model, response, xhr){
console.log(model,xhr, options, "success callback") //xhr is undefined
},
error: function(model, xhr,options){
console.log(model,xhr,options, "error callback")
}
}
Further Info
When the model's created, I do
this.model = new FunkyModel();
this.model.set({"type": "funky"})
When the model gets saved, it gets added to the collection (code shown above in original OP). I'm wondering if when I call this.model.destroy(), it's only destroying it in terms of removing it from the collection (hence the success callback is fired).
I'm trying to understand whether is necessary to unbind an event that is binded on the current instance of a view.
For instance when I do:
$(this.el).on('click', callback);
is it necessary to unbind the events (e.g. using off() or $(this).unbind('click') inside the callback function) or maybe the view will destroy the event and will give it to garbage collector?
You should setup all of your events via the View's events hash and only unbind them when removing a view via .remove().
Backbone Views use Event Delegation for the DOM Event handlers, so you can set up events for View HTML that doesn't exist yet, and, once the HTML is generated, the event handlers will catch the events as expected. All events handlers are attached to the views root element and watch for specific events that occur within that element or it's children.
Events will be unbound when you remove the view via Backbone.View.remove()
view.remove();
If you need to unbind events while the view is displayed (not common), you can specifically unbind that event via jQuery's .off(), but, you shouldn't have to (or want to) manage binding/unbinding your events.
The problem with manually unbinding events is that you may/probably will quickly find yourself conditionally unbinding and binding these event handlers according to user input. You'll go down the path of "unbind an event here, rebind it here, but unbind when this condition is true or this one is false"... it gets confusing, fragile and unmaintainable very quickly.
Instead, you should keep your DOM Bindings bound all of the time and have their execution dependent on the State of the view... sometimes the event handlers may do nothing, but that's fine. With this style of writing views, you're only concern with DOM events is that you remove your views properly. Inside the the view's state, you can consolidate the business logic behind when the views should respond to certain events.
What does State look like, in code?
initialize: function {
this.state = new Backbone.Model({ /* initial state */ });
}
Boom. It's that easy. State is just an object (or backbone model) where you can store data about the current state of the view. It's like the Views little junk drawer of useful data.
Should the save button be disabled?
this.state.set('save_button_disabled', true);
this.state.set('save_button_disabled', false);
Is the form validated? Errors?
this.state.set('form_valid', false);
this.state.set('form_errors', errorsArray);
Then bind some handlers to it, and when user does something, update the state and let the handlers handle it. Recording and responding to state changes will force you to write your views with lots of small functions that are easy to test, easy to name and easy to maintain. Having a dedicated object to store state is a great way to organize and consolidate the logic & conditions within the view.
this.listenTo(this.state, {
'change:save_button_disabled': this.toggleSaveButton,
'change:form_valid': this.onFormValidationChange
});
You can also tap into state within your views event handlers:
events: {
'click button.save': 'onSaveClicked'
},
onSaveClicked: function() {
if ( this.state.get('form_valid') ) {
/* do save logic */
}
}
As your application grows, you might also want to look into separating state into View State, Environment State (test, prod, dev, versions etc), User State (logged in/out, admin, permissions, users birthday? etc) and others. View State is usually the first step.
Your view should essentially be a bunch of small concise functions that respond to DOM, State and Model/Collection events. It should not contain the complex logic that evaluates, responds to and interprets user input and model data.. that complex stuff should exist in the Collections, Models and State. The view is simply a representation of those items and interface for the user to interact with them, just like the front-end of a web-app is an interface for the user to interact with a database.
View Code:
var MyView = Backbone.View.extend({
events: {
"click": "onClick"
},
"onClick": function() {
if ( this.state.get('clickable') ) {
/* do callback */
}
},
initialize: function(options) {
this.options = options;
this.state = new Backbone.Model({ clickable: true });
},
getTemplate: function() { /*...*/ },
render: function() {
var template = this.getTemplate(),
data = {
options: this.options,
data: this.model.toJSON(),
state: this.state.toJSON()
};
return this.$el.html(template(data));
}
});
Template Code:
{{#if state.logged_in}}
<p {{#if options.large}}class="font-big"{{/if}}>
Welcome back, {{data.userName}}.
</p>
{{else}}
<p>Hello! {{#sign_up_link}}</p>
{{/if}}
Here's a simple example: http://jsfiddle.net/CoryDanielson/o505ny1j/
More on state and this way of developing views
In a perfect world, a View is a merely a interactive representation of data and the many different states of an application. Views are state machines. The view is also a small buffer between the user and data. It should relay the user's intentions to models/collections as well as the model/collection responses back to the user.
More about this:
Model-View-Intent: http://futurice.com/blog/reactive-mvc-and-the-virtual-dom
Model-View-Intent slides: http://staltz.com/mvi-freaklies/#/
Also read the react.js docs to learn more about State as it applies to reactjs components which are an even lower-level representation of things than Backbone.Views. http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html
tl;dr
How to use backbone.stickit with a html form to change an existing model fetched from the server and only PATCH the changed attributes (changed by user input within the html form) to the server?
/tl;dr
I'm using backbone.stickit in a backbone.js application to bind a model to a HTML-form which is part of a backbone view. This works fine so far, but it becomes a little bit complicated if I'm going to save the bound model. This is because I want to use the PATCH-method and only send the changed attributes to the server. I try to illustrate what I've done so far:
Fetching the model from Server
user = new User(); //instatiate a new user-model
user.fetch(); //fetching the model from the server
console.log(user.changedAttributes()); // Returns ALL attributes, because model was empty
The last line indicates my problem, because I thought I can used the changedAtrributes() method later to get the attributes which need a patch on the server. So I tried this workaround which I found here
user.fetch({
success: function (model, response, options) {
model.set({});
}
});
user.changedAtrributes(); //Returns now "false"
Do stickit-bindings
Now I render my view and call the stickit() method on the view, to do the bindings:
//Bindings specified in the view:
[...]
bindings: {
"#username" : "username"
"#age" : "age"
}
[...]
//within the render method of the view
this.stickit();
The bindings work fine and my user model gets updated, but changedAttributes() remain empty all the time.
Save the model to the server
If the user has made all required changes, the model should be saved to the server. I want to use the PATCH method and only send the changed attributes to the server.
user.save(null, {patch:true}); //PATCH method is used but ALL attributes are sent to the server
OR
user.save(user.changedAttributes(),{patch : true});
With the second approach there are different outcomes:
if I didn't use the user.set({}) woraround, all attributes get PATCHED to the server
if I use the user.set({}) woraround the return value of changedAttributes() is "false" and all attributes are PUT to the server
if I call a user.set("age","123") before calling save(), then only the age attribute is PATCHED to the server
So outcome 3 is my desired behaviour, but there are 2 problems with this: First stickit doesn't seem to use the set() method on the model to update the attributes if they are changed within the html-form. And second, if you call set() with one attribute and afterwards with another, only the second attributes is returned by changedAttributes().
Maybe I just overseen something in the backbone or backbone.stickit docs, so I didn't get the desired behaviour working. Any ideas about that?
NOTE: As found out the problem wasn't directly related to backbone.stickit, more to backbone itself.
Solved this problem on my own, maybe this helps someone who may stumble upon this question:
Backbone only keep track of unchanged attributes, but not of unsaved attributes. So with
model.changedAttributes();
you will only get the attributes of the model, which was changed since the last
model.set("some_attribute","some_value")
Finally I stumbled upon backbone.trackit which is a backbone.js plugin maintained by the creator of backbone.stickit. With this plugin you can track unsaved attributes (all attributes which have changed since the last model.save()) and then use them in the save-method of a model. Example (my usecase):
Backbone.View.extend({
bindings: {
"#name" : "name",
"#age" : "age"
},
initialize: function () {
this.model = new User();
this.model.fetch({
success: function (model, response, options) {
//this tells backbone.stickit to track unsaved attributes
model.startTracking();
}
});
},
render: function () {
this.$el.html(tmpl);
this.stickit();
return this;
},
onSaveUserToServer: function () {
//first argument: only unsaved attributes, second argument: tell backbone to PATCH
this.model.save(this.model.unsavedAttributes(), { patch: true });
});
});
I have a form that create a model for a collection. That fires an add event to the collection. I have that binded to a method:
this.collection.bind('add', this.addOne, this)
fires...
addOne: function(tenant) {
var self = this
var collection = this.collection
var view = new TenantView({model: tenant,collection:collection});
self.$el.append(view.render().el);
}
The create syncs it to the database but, the new appended view still isNew to backbone since it hasn't fetched the collection and grabbed the id for the new model.
My question, how can I grab the synced model from the server (that has the id and isn't isNew) without fetching the entire collection then append it?
Use the sync event instead of add...
this.collection.bind('sync', this.addOne, this);
The add event gets fired immediately when calling create; but sync gets fired once the server has responded to the create method, so it should include the correct model id.
You can also wait for the server's response, before adding the model to the collection -- to do that use wait: true in create's options hash: collection.create({ ... }, { wait: true }).
Creating a model will cause an immediate "add" event to be triggered on the collection, as well as a "sync" event, once the model has been successfully created on the server. Pass {wait: true} if you'd like to wait for the server before adding the new model to the collection.
Im starting to build a new app and I would like to use Backbone as my framework. Below is a basic workflow that this (and most apps) follow.
What is the correct/best model to use with Backbone?
Old Way
User navigates to a page.
Selects "Create New widget"
User is presented with a form filled with inputs
At this point I would probably take the values entered (after passing basic validation), wrap them up and send them to the server via an ajax request
Request comes back as "OK" and the user is taken somewhere else (This step isn't entirely important)
Some basic pseudo-code
// Grab values
var userName = $('.UserName').val(),
dateOfBirth = $('.DateOfBirth').val();
...
...
...
$.ajax({
url: "/Webservices/ProcessStuff",
success: function(result){
if (result) {
// Render something or doing something else
} else {
// Error message
}
},
error: function () {
// Error message
}
});
Backbone way
Using the same example as above; I assume I'd have a model for the user information and a view to display the inputs. However, processing the actual call to the web service is one of the things I'm confused about. Where does this need to go? In the model or in the view click of some "Go" button?
Model.UserInformation = Backbone.Model.extend({ username: null, dateOfBirth: null });
Maybe also have a collection of these UserInformation models?
UserInformations = Backbone.Collection.extend({ model: Model.UserInformation' });
So bottom line what I'm asking is...
What is the best way to achieve this functionality?
What is the proper way to actually perform CRUD? Where to put the actual call to delete/update/create/etc?
You have the right idea and Backbone should make it easy for you to get things done using the same basic high level overview of your workflow. Note that you're still going to be using jQuery for this functionality - you'll just be doing it through the organizational aspects of Backbone's types.
There are a couple of key items that you'll want in place, most of which you already mentioned:
A backbone View to coordinate the HTML elements with your Javascript code
A backbone Model to store all of the data that the user input into the HTML elements
A back-end server that can handle RESTful JSON calls via AJAX requests from jQuery
I think the only thing you are missing is that the model has a save method on it, which wraps up all of the logic to call the create / update routes on your back-end server. The model also has a delete method to handle deletion from the server.
As a very simple example, here's a form that renders an HTML template to the screen, gathers the user input in to the model and then saves it to the server.
An HTML template:
<script id="myTemplate" type="text/x-jquery-tmpl">
First name: <input id="first_name"><br/>
Last Name: <input id="last_name"><br/>
<button id="save">Save!</button>
</script>
The code to run this:
MyModel = Backbone.Model.extend({
urlRoot: "/myModel"
});
MyView = Backbone.View.extend({
template: "#myTemplate",
events: {
"change #first_name": "setFirstName",
"change #last_name: "setLastName",
"click #save": "save"
},
initialize: function(){
_.bindAll(this, "saveSuccess", "saveError");
},
setFirstName: function(e){
var val = $(e.currentTarget).val();
this.model.set({first_name: val});
},
setLastName: function(e){
var val = $(e.currentTarget).val();
this.model.set({last_name: val});
},
save: function(e){
e.preventDefault(); // prevent the button click from posting back to the server
this.model.save(null, {success: this.saveSuccess, error: this.saveError);
},
saveSuccess: function(model, response){
// do things here after a successful save to the server
},
saveError: function(model, response){
// do things here after a failed save to the server
},
render: function(){
var html = $(this.template).tmpl();
$(el).html(html);
}
});
myModel = new MyModel();
myView = new MyView({model: myModel});
myView.render();
$("#someDivOnMyPage").html(myView.el);
This will give you a quick start for a form that saves a new model back to the server.
There are a couple of things your server needs to do:
Return a valid HTTP response code (200 or some other response that says everything was "ok")
Return the JSON that was sent to the server, including any data that the server assigned to the model such as an id field.
It's very important that your server do these things and include an id field in the response. Without an id field from the server, your model will never be able to update itself when you call save again. It will only try to create a new instance on the server again.
Backbone uses the id attribute of a model to determine if it should create or update a model when pushing data to the back end. The difference between creating a new model and saving one is only the id attribute. You call save on the model whether it's a new or an edited model.
A delete works the same way - you just call destroy on the model and it does a call back to the server to do the destroy. With some HTML that has a "delete" link or button, you would attach to the click event of that HTML element the same as I've shown for the "Save" button. Then in the callback method for the delete click, you would call this.model.destroy() and pass any parameters you want, such as success and error callbacks.
Note that I included a urlRoot on the model, as well. This, or a url function are needed on a model if the model is not part of a collection. If the model is part of a collection, the collection must specify the url.
I hope that helps.
If the "el" of the view is the form tag, then you could probably use the built in event object to bind a function to submit, but if the root of the view is something else, then you'll need to attach the click handler in the render function.