I think this is just a JavaScript scope question.
I'm trying to add some Jasmine tests to a Backbone application, but I can't figure out how to access Backbone models from within my Jasmine setup.
This is my current application structure (main.js is my Backbone application):
index.html
js/
main.js
vendor/
backbone.js
jquery.min.js // etc
tests/
SpecRunner.html
spec/
testSpec.js
The content of main.js is like this, and it's all running OK from index.html:
$(function(){
var Todo = Backbone.Model.extend({
defaults: function() {
return {};
},
... etc
The files in SpecRunner.html look like this:
<!-- include source files here... -->
<script src="/js/vendor/jquery-1.10.2.min.js"></script>
<script src="/js/vendor/underscore.js"></script>
<script src="/js/vendor/backbone.js"></script>
<script src="/js/main.js"></script>
<!-- include spec files here... -->
<script type="text/javascript" src="spec/testSpec.js"></script>
I have written this test in testSpec.js, but it's failing with ReferenceError: Todo is not defined:
describe("Todo tests", function(){
var todo = new Todo("Get the milk", "Tuesday");
it("should be correctly defined", function(){
expect(todo).toBeDefined();
});
it("should have the correct title", function(){
expect(todo.title).toBe("Get the milk");
});
});
How can I get hold of the Todo scope? I've tried window.Todo but that doesn't help either.
As we can see at the Backbone reference.
constructor / initialize new Model([attributes], [options])
When creating an instance of a model, you can pass in the initial values of
the attributes, which will be set on the model.
The constructor needs to be initilized with a object literal(Key value pair), as follow:
new Book({
title: "One Thousand and One Nights",
author: "Scheherazade"
});
So you need to change your code to:
var todo = new Todo({
task: "Get the milk",
dayOfWeek: "Tuesday"
});
UPDATE:
Here you declared the Todo inside the jquery function scope and you're trying to access from outside.
$(function(){
var Todo = Backbone.Model.extend({
defaults: function() {
return {};
},
etc...
}
}
You have two options, declare Todo as global, not recommend, but largely used at backbone samples:
$(function(){
window.Todo = Backbone.Model.extend({
defaults: function() {
return {};
},
etc...
}
});
You could also try to call the jquery function after declaration:
$(function(){
var Todo = Backbone.Model.extend({
defaults: function() {
return {};
},
etc...
}
})();
Related
Im new to backbone and I'm looking to a very simple 2 view configuration page usig backbone.
I have the following code;
define(
["backbone","...","..."],
function(Backbone, ... , ... ) {
var PopupView = Backbone.View.extend({
initialize: function initialize() {
Backbone.View.prototype.initialize.apply(this,arguments);
},
events: {
"click .save_conf_button": "save_conf",
},
render: function() {
this.el.innerHTML = this.get_popup_template();
return this;
},
save:conf: function save_conf() {
//get the field values from popup_template
//var items = jquery(....);
});
var ExampleView = Backbone.View.extend({
//Starting view
initialize: function initialize() {
Backbone.View.prototype.initialize.apply(this, arguments);
},
events: {
"click .setup_button": "trigger_setup", //Triggers final setup
"click .create_conf_button": "trigger_popup_setup", //This is the conf popup
},
render: function() {
this.el.innerHTML = this.get_start_html();
return this;
},
trigger_popup_setup: function trigger_popup_setup() {
console.log("Pop up");
//this.el.innerHTML = this.get_popup_template();
PopupView.render();
...
},
}); //End of exampleView
return ExampleView;
} // end of require asynch
); // end of require
E.g. The ExampleView is the starting view with a couple of fields and 2 buttons; create popup and save. Upon pressing the create_conf_button I want to render the popup view, however this does not seem to work as I expected. (Uncaught TypeError: PopupView.render is not a function)
I'm not sure how to proceed and additionally what the "best practice" is for generating these types of dialogs?
Additionally, keeping the values filled in on the previous page after returning from the popupview would be preferential.
Thanks for any help
try
new PopupView.render()
you have to create an instance to call the methods this way
#ashish is correct, you have to instantiate an instance of the PopupView before calling its render method. Currently, you have defined a blueprint for a view called PopupView, which will act as a constructor for newly created PopupView view instances. In order to use this defined view I would suggest storing it in ExampleView's render or initialize method:
// Example View's initialize method
initialize: function initialize() {
this.popUpView = new PopupView();
Backbone.View.prototype.initialize.apply(this, arguments);
},
then referencing it in your trigger_popup_setup function as follows:
trigger_popup_setup: function trigger_popup_setup() {
console.log("Pop up");
//this.el.innerHTML = this.get_popup_template();
this.popUpView.render();
...
},
As for storing state Backbone models are used for that :)
In general to nest subviews within a master view in Backbone you can do the following:
initialize : function () {
//...
},
render : function () {
this.$el.empty();
this.innerView1 = new Subview({options});
this.innerView2 = new Subview({options});
this.$('.inner-view-container')
.append(this.innerView1.el)
.append(this.innerView2.el);
}
In this example the master view is creating instances of it's subviews within its render method and attaching them to a corresponding DOM element.
I am new to Backbone.js. I have this project where there is a hierarchy of multiple views and sometimes the views need to communicate with each other.
After a little research on the Internet, I came across https://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/ and tried to use the event aggregator.
However, it didn't work. My guess is that the 'vent' is not the same instance across all the views. So, if there is any way to definine it as a static variable, I can probably make it work. So, is there a way to define static variables in Backbone.js?
Yes, it is possible to add static variables to any Backbone objects using extend:
obj.extend( protoProps, staticProps );
e.g.:
var MyView = Backbone.View.extend( { events: ... }, {
myStaticVar: 'anything'
} );
...
var value = MyView.myStaticVar;
However, I am not sure you need this in this case.
Let's see. Not quite sure what was that example you showed, daga (but - Japanese version) if you want to make a static something in Bb.js(Backbone.js) u'll have to use some tricks.
For example you got a list of bb.views:
[Code #01]
01|var ViewX01 = Backbone.View.extend({
02| initialize: function(){
03| console.log('ViewX01');
04| this.addToList();
05| },
06| addToList : function(){
07| ViewStaticStyle.elEventos.push(this);
08| console.log('Push #1: ', ViewStaticStyle.elEventos);
09| }
10|});
11|var ViewX02 = Backbone.View.extend({
12| initialize: function(){
13| console.log('ViewX02');
14| this.addToList();
15| },
16| addToList : function(){
17| ViewStaticStyle.elEventos.push(this);
18| console.log('Push #2: ', ViewStaticStyle.elEventos);
19| }
20|});
21|var ViewX03 = Backbone.View.extend({
22| initialize: function(){
23| console.log('ViewX03');
24| this.addToList();
25| },
26| addToList : function(){
27| ViewStaticStyle.elEventos.push(this);
28| console.log('Push #3: ', ViewStaticStyle.elEventos);
29| }
30|});
31|var ViewX04 = Backbone.View.extend({
32| initialize: function(){
33| console.log('ViewX04');
34| this.addToList();
35| },
36| addToList : function(){
37| ViewStaticStyle.elEventos.push(this);
38| console.log('Push #4: ', ViewStaticStyle.elEventos);
39| }
40|});
A view/model that will imitate a static variable upon initialization:
[Code #02]
01|var ViewStaticStyle = Backbone.View.extend({
02| initialize: function(){
03| console.log('Init static variable');
04| ViewStaticStyle.elEventos = []; // <- This one is a static array ;)
05| }
06|});
And a master view that inits them all except for "ViewStaticStyle":
[Code #03]
01|var MasterView = Backbone.View.extend({
02| initialize: function(){
03| // Instantiate each and store inside the MasterView
04| this.view1 = new ViewX01();
05| this.view2 = new ViewX02();
06| this.view3 = new ViewX03();
07| this.view4 = new ViewX04();
08| }
09|});
Then upon $(document).ready():
[Code #04]
01|// Instantiate the static array
02|var Eventitoz = new ViewStaticStyle();
03|// Instantiate the MasterView, add it to the window in case you
04|// want to test the "static" thing further via browser console ;)
05|window.App = new MasterView();
So the [Code 02] Line [04] Is the "static" thing.
Hope it helped.
A source code would be provided upon request.
This question already has an answer here:
Unable to display Todo Collection on the page
(1 answer)
Closed 8 years ago.
In the below code, unable to render 'TodoList'. Seems like fetching taking time and so displaying '0' and <div id="demo"></div> before only.
and Iam not sure why '3' and 'Descriptions' got displayed later. All I need is to display 'Descriptions List' in the page. Iam able to get data from server but somehow not able to display as soon as the data arrived. Please tell me what changes need to do in the below code?
<html>
<head>
<link rel="stylesheet"
href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.1.1/css/bootstrap.min.css">
</head>
<body>
<div id="demo"></div>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
<script type="text/javascript">
var TodoItem = Backbone.Model.extend({
urlRoot: 'api',
})
var TodoCollection = Backbone.Collection.extend({
model: TodoItem,
url: 'api/todos'
})
var TodoView = Backbone.View.extend({
template: _.template('<h3> ' +'<input type=checkbox ' +'<% if(status === "complete") print("checked") %>/>' +' <%= description %></h3>'),
render: function(){
this.$el.html(this.template(this.model.toJSON()))
}
})
var TodoListView = Backbone.View.extend({
initialize: function(){
this.listenTo(this.collection,'reset',this.render)
this.collection.fetch({reset:true})
},
render: function(){
console.log(this.collection.length)
this.collection.forEach(this.addOne,this)
},
addOne: function(todoItem){
console.log(todoItem.get('description'))
var todoView = new TodoView({model: todoItem})
this.$el.append(todoView.render())
}
})
var todoItem = new TodoItem()
var todoList = new TodoCollection()
var todoListView = new TodoListView({el: '#demo', collection: todoList})
todoListView.render()
console.log(todoListView.el)
</script>
</body>
</html>
Here is the CONSOLE output Iam getting:
0
<div id="demo"></div>
3
pick up cookies
Milk
Cookies
For starters you might want to take out the {reset: true} from your fetch.
A fetch wil automatically clear the model/collection anyway.
Please also use semicolons at the end of your command, not using them will let the browser interpret where the semicolon should be. This takes time and is error prone (the browser might just place it where you didn't think it would).
if this does not work you might want to do add the fetch into the render doing this:
render: function(){
var that = this;
this.collection.fetch().done(function(data) {
console.log(that.collection.length);
that.collection.forEach(that.addOne,that);
});
},
What also might work, but you need to test this, I personally always use the one above:
render: function(){
this.collection.fetch().done(function(data) {
console.log(this.collection.length);
this.collection.forEach(this.addOne,this);
}, this);
},
and Iam not sure why '3' and 'Descriptions' got displayed later - Because it the result of a async Ajax request.
now, try to change your code (watch comment):
var TodoView = Backbone.View.extend({
template: _.template('<h3> ' +'<input type=checkbox ' +'<% if(status === "complete") print("checked") %>/>' +' <%= description %></h3>'),
clearItem : function(){
this.$el.find("h3").remove();
},
render: function(){
//all DOM manipulation in view
this.$el.append(this.template(this.model.attributes));
return this;
}
})
var TodoListView = Backbone.View.extend({
initialize: function(){
// split "reset" event and "add" event
this.listenTo(this.collection,'reset',this.removeAll);
this.listenTo(this.collection,'add',this.addOne);
this.collection.fetch({reset:true});
},
removeAll : function(){
//method to remove all element from view
//your problem is that this event will fire before ajax request done
console.log("reset!");
var todoView = new TodoView();
todoView.clearItem();
},
addOne: function(todoItem){
//fire when a model in the collection change (automatic after fetch result) for each model.
console.log("add ITEM:",todoItem);
var todoView = new TodoView({model: todoItem})
todoView.render();
}
});
NOTE: Remove todoListView.render() in your code.
Sorry but, my english is too bad. I do not have time to explain better. Try if my code work
EVENTS in backbone: http://backbonejs.org/#Events
FETCH in collection: http://backbonejs.org/#Collection-fetch
RENDER a view: http://backbonejs.org/#View-render
Im new to Backbone (dont hate me) but am pulling my hair out trying to do a very simple thing.
Im loading a json file (correctly as I can see it loading in firebug) and I just want to pull some info from it purely for testing (as its my first backbone code)
However, I cant get this working and end up with one blank li tag (code below)
<ul id="phones"></ul>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.2/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.rc.1/handlebars.min.js"></script>
<script id="foo" type="text/template">
<li><%= name %></li>
</script>
<script>
var Phones = Backbone.Collection.extend({
url:'http://backbone.local/phones/phones.json'
})
var PhonesView = Backbone.View.extend({
el:'#phones',
initialize:function(){
this.collection = new Phones();
this.collection.fetch();
this.render();
},
template:_.template($('#foo').html()),
render:function(){
var foo = this.collection.toJSON();
$(this.el).html(this.template(foo));
return this;
}
})
var phonesView = new PhonesView();
</script>
Any pointers greatly appreciated.
Cheers,
UPDATE 1
I thought it may be due to fetch being async so i called render in success callback of fetch as below. The console.log fires fine but still no json data in rendered html (i also changed to using handlebars)
<script>
var Phones = Backbone.Collection.extend({
url:'http://backbone.local/phones/phones.json'
})
var PhonesView = Backbone.View.extend({
el:'#phones',
initialize:function(){
var self = this;
this.collection = new Phones();
this.collection.fetch({
success:function(){
console.log('json loaded');
self.render();
}
});
},
template: Handlebars.compile('<li>sdsadsadsadsad {{name}} dsfcdfd</li>'),
render:function(){
var foo = this.collection.toJSON();
$(this.el).html(this.template(foo));
return this;
}
})
var phonesView = new PhonesView();
</script>
With Handlebars, a collection template looks like this:
{{#items}} <li> {{name}} </li> {{/items}}
You also need to wrap your collection JSON in an items object so that the Handlebars template can reference it as above:
var foo = { items: this.collection.toJSON() };
Edit
There's actually one more issue ... collection.toJSON() doesn't convert each model to JSON. So you need to write:
this.collection.models.map(function(x) { return x.toJSON(); });
Fiddle Demo
On your view:
'initialize': function() {
this.template = _.template('<p><% model.text $></p>');
this.collection.fetch({error: function() { console.log(arguments); }, 'success': _.bind(this.onFetch,this) });
return this;
},
'onFetch': function(collection) {
this.collection.on("add", this.onAdd, this);
this.collection.each( _.bind(this.onAdd, this) );
return this;
},
'onAdd': function(model){
//do something like:
this.$el.find('.items').append(this.template({model: model}) )
);
To answer your explicit question of why you only get an empty li. You must send the template some name data. For example:
render:function(){
var firstModel = this.collection.at(0);
var firstName = firstModel.get("name");
$(this.el).html(this.template({name: firstName}));
return this;
}
Of course, the above code is only to understand what's missing, and not how a backbone application should be implemented. I really recommend that you go over the annotated TODO example linked from Backbone's website, and understand the basic patterns that are implemented there.
Update based on your comment:
Different ways of solving this. Really recommend reading: http://backbonejs.org/docs/todos.html
To continue on the "hacky path" of solving this so you can see something:
addOne: function(phone) {
this.$el.append(this.template(phone.toJSON()));
return this;
},
render: function() {
this.$el.html(); //clear everything in the view
this.collection.forEach(_.bind(this.addOne,this)); //Will call addOne for every model in the collection.
return this;
}
Hope you can have a quick look at what I'm doing here. Essentially, am I doing it right?
Live demo of it here too: http://littlejim.co.uk/code/backbone/messing-around/
I just wanted to get a solid understanding in Backbone before I go too wild. So this is a simple demonstration of creating a collection from a JSON object, passing it to a view and handling simple events. But am I approaching this right? What can I do that's better?
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Showing a simple view with events</title>
<script type="text/javascript" src="../../media/scripts/jquery-1.5.1.min.js"></script>
<script type="text/javascript" src="../../media/scripts/underscore-min.js"></script>
<script type="text/javascript" src="../../media/scripts/backbone-min.js"></script>
<script type="text/javascript" src="application.js"></script>
</head>
<body>
<header>
<h1>Showing views from a collection and basic events</h1>
<p>The list below is made from JSON, passed to the view as a collection and has basic events</p>
</header>
<article>
</article>
</body>
</html>
Here is the JavaScript I currently have. I just need to know if I'm approaching this correctly?
window.App = {
// namespaces
Controller: {},
Model : {},
Collection : {},
View : {},
// code that starts when the app is first fired
initialize : function () {
var collection = new App.Collection.Inputs([
{title: "Item 1"},
{title: "Item 2"},
{title: "Item 3"}
]);
var view = new App.View.InputSet({collection: collection});
$('article').html(view.render().el);
}
}
/*
Collection: Inputs */
App.Collection.Inputs = Backbone.Collection.extend();
/*
View: _Input */
App.View._Input = Backbone.View.extend({
events: {
"click a": "close"
},
// called as soon as a view instance is made
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render", "close");
},
// backbone required method, which renders the UI
render: function() {
// this is using underscore templating, which can be passed context
$(this.el).html(_.template('<p><%=title%> [close]</p>', this.model.toJSON()));
return this;
},
close: function() {
// removes the UI element from the page
$(this.el).fadeOut(300);
return false; // don't want click to actually happen
}
});
/*
View: InputSet, uses _Input */
App.View.InputSet = Backbone.View.extend({
events: {
'click a': 'clear'
},
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render");
},
// backbone required method, which renders the UI
render: function() {
var that = this;
views = this.collection.map(function(model) {
var view = new App.View._Input({model: model});
$(that.el).append(view.render().el);
return view;
});
$(that.el).append('[clear]');
return this;
},
clear: function() {
$(this.el).find('p').fadeOut(300);
}
});
// wait for the dom to load
$(document).ready(function() {
// this isn't backbone. this is running our earlier defined initialize in App
App.initialize();
});
This looks fine to me. However, I found that things can get tricky once you start doing non-trivial stuff: complex views, nested collections etc.
One thing that could be done differently is that instead of generating input views using collection.map you could bind the collection's add event to a function that generates an _Input view for that item in the collection instead. So you'd have something like this in your InputSet view:
initialize: function() {
_.bindAll(this, "addInput", "removeInput");
this.collection.bind("add", this.addInput);
this.collection.bind("remove", this.removeInput);
}
addInput: function(model) {
var view = new App.View._Input({model: model});
$(this.el).append(view.render().el);
}
I looks good to me - really the only thing I would suggest is that you bind the collection's 'change' event to _Input.render that way changes to your collection automatically re-render the view:
// called as soon as a view instance is made
initialize: function() {
_.bindAll(this, "render", "close");
this.collection.bind('change', this.render);
},
Other than that I think it looks good!