I'm been studying Java at school but decided to try JavaScript and have been applying for a JavaScript bootcamp. I've spent a lot of time studying JavaScript to complete the application but never before have I experienced something so mind-bendingly, make-me-want-to-give-up-on-being-a-programmer frustrating as learning Backbone.js.
I have struggled for hours and hours to make a simple program but right now when I fill in information and click submit it creates multiple copies of the el on the page with the entered info, then disappears. I have no idea why either is happening except that the "view instantiated" console log I put in displays both then starting the program and when I click submit.
The relevant html:
<container class="app">
<form id="userInfo">
<table>
<tr><td>First name:</td> <td><input type="text" id="firstName" required></td></tr>
<tr><td>Last name:</td> <td><input type="text" id="lastName" ></td></tr>
<tr><td>E-mail:</td> <td><input type="email" id="email"></td></tr>
<tr><td><button class="addUser" type="submit">add user</button></td></tr>
</table>
</form>
<span class="users"></span>
</container>
The model:
var User = Backbone.Model.extend({
defaults: function(){
return {
firstName: 'test',
lastName: '',
email: ''
}
}
});
The view:
var UserView = Backbone.View.extend({
template: _.template( "<div class='item'>First name: <%= firstName %><br>Last name: <%= lastName %> <br>E-mail: <%= email %><br><button class='delete' type='submit'>delete user</button><button class='edit' type='submit'>edit</button></div>"),
el: '.app',
initialize: function(){
console.log("view instantiated");
_.bindAll(this, 'render', 'createUser');
this.model.on('change', this.render);
},
events: {
'submit form': 'createUser',
'click .delete': 'deleteUser',
'click .edit': 'editUser'
},
render: function(){
$('.users').append(this.template(this.model.toJSON()));
console.log("changed to " + this.template(this.model.toJSON()));
return this;
},
createUser : function(){
alert("adding user");
this.model.set({'firstName': $("#firstName").val()});
this.model.set({'lastName': $("#lastName").val()});
this.model.set({'email': $("#email").val()});
this.render();
},
deleteUser: function(e){
alert('delete user');
},
editUser: function(e){
alert('edit user');
},
});
var user = new User;
var userView = new UserView({model: user});
There is still a lot of functionality I have to put in this before it's done. Any help would be greatly appreciated. Describing this project as Sisyphian is an understatement.
Edit: Okay, to clarify, I want to create a system where someone can enter a first/last name and email to create a user, have a box with that information show up on screen. So far that part is finished, except several boxes are created when you click submit and then they all minimize. Eventually the you will be able to create multiple users and edit or delete them. This user information doesn't connect with anything, just lives in the browser.
As you mentioned there are 2 issues lets us tackle them one by one.
First issue : multiple copies of el on the page with entered info.
In intialize method, you have registered for 'change' event on the model, which means any attribute of model changes it calls the render method.
this.model.on('change', this.render);
In render method, the template is inflated and appended to '.users' element.
$('.users').append(this.template(this.model.toJSON()));
The createUser method is called, whenever form is submitted. In this method, set method is called 3 times, which will trigger the model 'change' event 3 times, which will result in render method being called 3 times. Additionally the render method is called explicitly. Thus 4 copies of el appear on the page.
createUser : function(){
alert("adding user");
this.model.set({'firstName': $("#firstName").val()});
this.model.set({'lastName': $("#lastName").val()});
this.model.set({'email': $("#email").val()});
this.render();
},
To overcome this issue, remove on change event registration from initialize method.
Second issue: then disappears
It is happening due to form submission. To avoid that you need to call preventDefault method on the event. Update creaUser code as shown below:
createUser : function(event){
event.preventDefault();
//Rest of your code can come here
},
In Javascript your object constructors are functions which must be called, so when you do:
var user = new User;
You are not actually creating a new user. Instead, you are copying all of the attributes and methods of your constructor function into user, effectively creating a new symbol for the same constructor function.
In order to create a new user, therefore, you would have to do this:
var user = new User();
If you have some arguments you wanted to pass in, you could do so:
var user = new User({'first name': 'Jimbo', 'last name': 'Uncleperson'});
You don't usually set up defaults and return them. Instead, model definitions usually look something like:
var User = Backbone.Model.extend({
defaults: {
firstName: 'test',
lastName: '',
email: ''
},
someMethod: function () { // here we can define some other functionality
console.log(this.firstName);
}
});
The defaults are used for any values that are not supplied.
After that, when you pass your user object to your view, it should be able to operate on it:
var userView = new UserView({model: user});
Related
I have a form (on localhost) with 2 fields:
First Name (text box)
Last Name (text box)
Once the form is submitted, I need to use API - https://beta.test.com/api
The documentation says -
"POST /user will add the details to system and generates a user ID which would be returned."
After I receive user ID in response, I need to call another endpoint -
"POST /user/metadata will fetch the metadata for a previously added user."
I have to build this in backbonejs. What should be my approach? Do you have any tutorials which I can look at?
I did some code but it gave me - "Access-Control-Allow-Origin". I have checked on server and the API already has cross domain allowed for all.
Please suggest.
For a good example look at TODO app in backbone way.
I will also suggest you to read Backbone's documentation and view the source code.
It will documented so you can find all you need there, if no look into the source.
Simple implementation of your form could be achieved like this:
For interaction with API and data exchange via REST create User model:
var UserModel = Backbone.Model.extend({
urlRoot: 'your/api/path',
default: { // this will be setted when model attributes are empty
firstname: 'Default Name',
lastname: 'Default Lastname'
}
});
Form view which will render you form and will bind model's attributes to the form's elements:
var UserForm = Backbone.View.extend({
initialize: function() {
this.render();
},
el: '.form-container', // this will attach view to the DOM's element with 'form-container' class
template: _.template($('#user-form').html()),
events: {
'submit': 'onFormSubmitted',
// validation logic could be added here
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
},
onFormSubmitted: function(e) {
e.preventDefault(); // we don't need to submit the form
// get form elements here and setting on model
// saving model at the end
var firstName = this.$('input[name="firstname"]').val();
var lastName = this.$('input[name="lastName"]').val();
this.model.set({firstname: firstName, lastname: lastName});
this.model.save(); // this will make POST request to your API
}
});
And then initialize you view and pass User model.
var userForm = new UserForm({model: new UserModel()});
I have left the declaration of template for you.
There is a lot of staff for cross origin requests policy issues when using Backbone. Actually it's not the Backbone thing. Backbone uses $.ajax to interact with REST-full resources. So you just need to configure $.ajax. Look here.
I'm working on a backbone.js project which is mainly to learn backbone framework itself.
However I'm stuck at this problem which i can't figure out but might have an idea about the problem...
I've got an Create View looking like this...
define(['backbone', 'underscore', 'jade!templates/addAccount', 'models/accountmodel', 'common/serializeObject'],
function(Backbone, underscore, template, AccountModel, SerializeObject){
return Backbone.View.extend({
//Templates
template: template,
//Constructor
initialize: function(){
this.accCollection = this.options.accCollection;
},
//Events
events: {
'submit .add-account-form': 'saveAccount'
},
//Event functions
saveAccount: function(ev){
ev.preventDefault();
//Using common/serializeObject function to get a JSON data object from form
var myObj = $(ev.currentTarget).serializeObject();
console.log("Saving!");
this.accCollection.create(new AccountModel(myObj), {
success: function(){
myObj = null;
this.close();
Backbone.history.navigate('accounts', {trigger:true});
},
error: function(){
//show 500?
}
});
},
//Display functions
render: function(){
$('.currentPage').html("<h3>Accounts <span class='glyphicon glyphicon-chevron-right'> </span> New Account</h3>");
//Render it in jade template
this.$el.html(this.template());
return this;
}
});
});
The problem is that for every single time I visit the create page and go to another and visit it again. It remebers it, it seems. And when i finally create a new account I get that many times I've visited total number of accounts...
So console.log("Saving!"); in saveAccount function is called x times visited page...
Do I have to close/delete current view when leaving it or what is this?
EDIT
Here's a part of the route where i init my view..
"account/new" : function(){
var accCollection = new AccountCollection();
this.nav(new CreateAccountView({el:'.content', accCollection:accCollection}));
console.log("new account");
},
/Regards
You have zombie views. Every time you do this:
new CreateAccountView({el:'.content', accCollection:accCollection})
you're attaching an event listener to .content but nothing seems to be detaching it. The usual approach is to call remove on a view to remove it from the DOM and tell it to clean up after itself. The default remove does things you don't want it to:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
You don't want that this.$el.remove() call since your view is not responsible for creating its own el, you probably want:
remove: function() {
this.$el.empty(); // Remove the content we added.
this.undelegateEvents(); // Unbind your event handler.
this.stopListening();
return this;
}
Then your router can keep track of the currently open view and remove it before throwing up another one with things like this:
if(this.currentView)
this.currentView.remove();
this.currentView = new CreateAccountView({ ... });
this.nav(this.currentView);
While I'm here, your code will break as soon as you upgrade your Backbone. As of version 1.1:
Backbone Views no longer automatically attach options passed to the constructor as this.options, but you can do it yourself if you prefer.
So your initialize:
initialize: function(){
this.accCollection = this.options.accCollection;
},
won't work in 1.1+. However, some options are automatically copied:
constructor / initialize new View([options])
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.
so you could toss out your initialize, refer to this.collection instead of this.accCollection inside the view, and instantiate the view using:
new CreateAccountView({el: '.content', collection: accCollection})
I am working on MVC application with Backbone.js.
Assuming, I have a View of User details:-
var userDetailsView = Backbone.View.extend({
model: userModel,
el: "#userDteails",
template: Handlebars.templtas.userDetails
initialize: function () {
this.model = new userModel();
this.model.fetch({
success: function (data) {
this.render();
}
});
},
render: function () {
$(this.el).html(this.template());
},
events: {
"", "saveUserDetails" //event for save
},
saveUserDetails: function () {
//How do I get the update value of FirstName??
}
});
Now, in similar line I have a handlebar template which deals with edit details of User Model.
<div id="userDetails">
<input type="text" value="{{FirstName}}" id="firstName"/>
</div>
Please ignore the code mistakes as its just a dummy code, now if I need to save the user details(say for eg. FirstName). Then how do I get the updated value?
Should it be:-
saveUserDetails: function () {
//How do I get the update value of FirstName??
this.model.set("", $('#Firstname').val());
}
or should I follow converting form data to JSON, and update my this.model i.e create my HTML markup with name attribute:-
<form>
First Name:<input type="text" name="Fname" maxlength="12" size="12"/> <br/>
</form>
and use the function suggested by Tobias Cohen
$.fn.serializeObject = function()
{
var o = {};
var a = this.serializeArray();
$.each(a, function() {
if (o[this.name] !== undefined) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value || '');
} else {
o[this.name] = this.value || '';
}
});
return o;
};
and do :-
$('form').submit(function() {
$('#result').text(JSON.stringify($('form').serializeObject()));
return false;
});
I am not an expert in Backbone, but have seen at-least 10-15 samples and tutorials which teach backbone. Out of those I found the above two way to do it.
Please let me know, what would be best way to proceed.
Both ways are OK! It depends on how complex your HTML <-> Model mapping is. If everything can be done with div/span/input with a name/data-name or whatever floats your boat, then the serializing route is straightforward. But once you grow out of that, you'll probably look at more custom ways, which is technically similar to your first option, with a bit more logic that just getting the .val() from the input field.
I don't really understand both of your example handling tho, but you said to not worry about the details... so :) For the form one, I assume that the .text() is just for debugging purpose? The correct code would probably first preventDefault() on the form submitting and then do a model.save($('form').serializeObject()) to both update the model and save it on the server at the same time. With some success/error call back thrown in for good measure!
I'm using the Backbone Layout Manager Boilerplate. Unfortunately, a quite frustrating bug occurred. I like render a list of items as subviews inserted by insertView function. At the first load everthing works fine. But after a reload the the click events doesn't work anymore :(. I already tried to call delegateEvents() on the TableItem View manually but nothing changed. I hope anyone can give me a clue.
App.Views.Item = Backbone.View.extend({
template: "templates/item",
tagName: "li",
events: {
"click .applyButton" : "apply",
"click .viewDetailsButton" : "showDetail"
},
serialize: function() {
return { table : this.model.toJSON() };
},
apply: function(ev) {
ev.preventDefault();
alert("apply button clicked");
},
showDetail: function(ev) {
ev.preventDefault();
var id = this.model.get("_id");
app.router.navigate("#events/"+ id, {trigger : true})
}
});
/*
* List View
*/
App.Views.List = Backbone.View.extend({
template: "templates/list",
tagNam: "ul",
className: "tableList",
beforeRender: function() {
var events = this.model.get("userEvents").get("hosting");
events.each(function(model) {
this.insertView(new App.Views.Item({ model : model }));
}, this);
},
serialize: function() {
return {};
}
});
I think you might want to add a cleanup function on your Item view to undelegate the events when layoutmanager removes the view. I don't know if this will fix your problem, but it seems like good practise.
When you say after a reload, do you mean reloading the page with the browser reload button? if so, how do you get it to work in the first place?
It would help if you could provide a jsfiddle of your setup, or point us to a repo so we can test it on our machines. Make sure you include the router so that we can have a look at how the view and the layout that contains it are initialised.
I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.