How to use JSON to power interactive Backbone.js app - javascript

I'm working on converting a series of interactive educational apps from Flash to Javascript, and my team is planning on using Backbone.js as the framework. Each of these apps is basically a collection of scenes that present information to the user and/or prompt some interaction, either in the form of questions or interactive widgets. The basic structure we're considering for the app is as follows:
a set of JSON files that contain the particular information for each app, such as how many different "scenes" or objects the app has, the different messages and/or widgets displayed to users, etc.
a set of Backbone (probably Underscore) templates governing how to display navigation, messages, etc.
a collection of Backbone views / routers / models to facilitate navigating between scenes in an app and handling user interaction
some interactive widgets built in native Javascript
Trouble is, of course, is that I'm a novice when it comes to Backbone. I've made my way through some of the basic tutorials but am having trouble integrating Backbone with static JSON files.
Let's say I have the following very basic JSON file that lays out three scenes to be displayed:
var scenes = [
{
"name": "Introduction",
"label": "Introduction",
"message": "Welcome to this app"
},
{
"name": "Exercise",
"label": "Exercise",
"message": "If this were a real app, there'd be some sort of exercise here"
},
{
"name": "Conclusion",
"label": "Conclusion",
"order": "Thank you for completing this app"
}
]
What I need, and what I'm trying to do, is to have Backbone generate a navigation widget that lets users navigate between these scenes and to display the message for each scene. (This is obviously an incredibly simplified version of the real-world app.)
Here's what I've tried:
// simplified object containing stage information
var stages = [
{
"name": "Introduction",
"label": "Introduction",
"message": "Welcome to this app"
},
{
"name": "Exercise",
"label": "Exercise",
"message": "If this were a real app, there'd be some sort of exercise here"
},
{
"name": "Conclusion",
"label": "Conclusion",
"order": "Thank you for completing this app"
}
];
$(function(){
// create model for each stage
StageModel = Backbone.Model.extend({});
// create collection for StageModel
StageModelList = Backbone.Collection.extend({
model: StageModel
});
var stageModelList = new StageModelList();
// create view for list of stages
StageListView = Backbone.View.extend({
el: $("#stageNav"),
initialize: function() {
// if stages are added later
stagemodellist.bind('add',this.createStageList, this);
},
events: {
'click .stageListItem' : 'selectStage'
},
createStageList: function(model) {
$("#stageList").append("<li class='stageListItem'>"+model.label+"</li>");
},
selectStage: function() {
this.router.navigate("stage/"+this.stage.name,true);
}
});
// create view for each stages
StageView = Backbone.View.extend({
el: $("#stage"),
initialize: function(options) {
// get stage variable from options
this.stage = this.options.stage;
// display stage
createOnEnter(this.stage);
},
createOnEnter: function(stage) {
$("#stageLabel").html(stage.label);
$("#stageMsg").html(stage.message);
}
});
// create router
AppRouter = Backbone.Router.extend({
initialize: function() {
Backbone.history.start();
// create collection
new StageModelList();
// create view when router is initialized
new StageListView();
// loop through stages and add each to StageModelList
for (var s in stages) {
StageModelList.add(stages[s]);
}
},
routes: {
"stage/:stage" : "renderStage"
},
renderStage: function(stage) {
// display StageView for this stage
new StageView({stage:stage});
}
});
var App = new AppRouter();
});
And the html:
<!DOCTYPE html>
<html>
<head>
<script class="jsbin" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script class="jsbin" src="http://documentcloud.github.com/underscore/underscore-min.js"></script>
<script class="jsbin" src="http://documentcloud.github.com/backbone/backbone.js"></script>
<script src="js/ilo4.js"></script>
<meta charset=utf-8 />
<title>JS Bin</title>
</head>
<body>
<p>My pathetic attempt at a Backbone.js app</p>
<div id="stageNav">
<ul id="stageList">
</ul>
</div>
<div id="stage">
<div id="stageLabel">
</div>
<div id="stageMsg">
</div>
</div>
</body>
</html>
(You can also see a jsbin version here: http://jsbin.com/iwerek/edit#javascript,html,live).
Right now this doesn't do anything, unfortunately.
I know that I'm doing so many things wrong here, and some questions that I'm kicking around:
Do I even need a router?
Do I need to initialize the collection as a variable?
Is there a better way to bind the model to the list of stages?
A

You were actually not too far off.
I've cloned your jsbin and fixed it up so it works: link
I submit that as my answer to your question. I've commented it pretty thoroughly to explain what's going on.
Take a look, hopefully it helps.
EDIT: what the hell, I'll put the code here as well:
// simplified object containing stage information
window.stages = [
{
"name": "Introduction",
"label": "Introduction",
"message": "Welcome to this app"
},
{
"name": "Exercise",
"label": "Exercise",
"message": "If this were a real app, there'd be some sort of exercise here"
},
{
"name": "Conclusion",
"label": "Conclusion",
"message": "Thank you for completing this app"
}
];
$(function(){
// StageModel: no need to extend if you're not adding anything.
StageModel = Backbone.Model;
// StageCollection
StageCollection = Backbone.Collection.extend({
model: StageModel
});
// create view for list of stages
StageCollectionView = Backbone.View.extend({
el: $("#stageNav"),
initialize: function() {
// if stages are added later
this.collection.bind('add', this.createStageListItem, this);
},
events: {
'click .stageListItem' : 'selectStage'
},
// I'm adding the model's cid (generated by backbone) as the
// id of the 'li' here. Very non-ideal, as one of the points
// of backbone et al. is to keep from embedding and retrieving
// data from the DOM like this.
//
// Perhaps better would be to create a StageListItemView and
// render one for each model in the collection, perhaps like:
//
// createStageListItem: function(model) {
// this.$('#stageList').append(new StageListItemView({model: model});
// }
//
// where you have a StageListItemView that knows how to render
// itself and can handle click events and communicate with the
// collectionview via events.
//
// At any rate, this string-munging will suffice for now.
createStageListItem: function(model) {
this.$("#stageList").append("<li id=\"" + model.cid + "\" class='stageListItem'>" + model.get('label') + "</li>");
},
// Use backbone's event system, it's pretty awesome. Not to mention
// that it helps to decouple the parts of your app.
//
// And note that you can pass arguments when you trigger an event.
// So any event handler for the 'new-stage' event would receive
// this model as its first argument.
selectStage: function(event) {
var cid = $(event.target).attr('id');
this.trigger('new-stage', this.collection.getByCid(cid));
},
// This was a missing puzzle piece. Your StageCollectionView wasn't
// being rendered at all.
//
// Backbone convention is to call this function render, but you could
// call it whatever you want, as long as you, well, end up _calling_ it.
render: function() {
var self = this;
this.collection.each(function(model) {
self.createStageListItem(model);
});
return this;
}
});
// StageView,
StageView = Backbone.View.extend({
el: $("#stage"),
// We're going to assume here that we get passed a
// newStageEventSource property in the options and
// that it will fire a 'new-stage' event when we need
// to load a new stage.
initialize: function(options) {
this.eventSource = options.newStageEventSource;
this.eventSource.bind('new-stage', this.loadAndRenderStage, this);
},
// A load function to set the StageView's model.
load: function(model) {
this.model = model;
return this;
},
render: function() {
$("#stageLabel").html(this.model.get('label'));
$("#stageMsg").html(this.model.get('message'));
},
loadAndRenderStage: function(stage) {
this.load(stage);
this.render();
}
});
// Instatiate a StageCollection from the JSON list of stages.
// See Backbone docs for more, but you can pass in a list of
// hashes, and the Collection will use its model attribute to
// make the models for you
var stageCollection = new StageCollection(stages);
// View constructors take an options argument. Certain properties
// will automatically get attached to the view instance directly,
// like 'el', 'id', 'tagName', 'className', 'model', 'collection'.
var stageCollectionView = new StageCollectionView({
collection: stageCollection
});
// Instantiate the StageView, passing it the stageCollectionView in
// the options for it to listen to.
var stageView = new StageView({
newStageEventSource: stageCollectionView
});
// Last step, we need to call 'render' on the stageCollectionView
// to tell it to show itself.
stageCollectionView.render();
});

Related

Passing a model to a LayoutView Backbone.Marionette

I am attempting to pass a model to a LayoutView so that the particular model attributes can be edited in the view.
In my ItemView I have an event that grabs the selected model and assigns it to my global App-
var ItemView = Backbone.Marionette.ItemView.extend({
template: _.template(ItemTemplate),
tagName: "tr",
attributes: function(){
return {
"id": this.model.get('id'),
"class": "tr"
};
},
events: {
"click #edit" : "edit"
},
edit: function() {
App.EditModel = this.model;
console.log(App.EditModel); // <-- prints out the model successfully
App.trigger('edit');
}
})
Then, I am using the App.EditModel in my edit view to pass to my template-
var Layout = Backbone.Marionette.LayoutView.extend({
model: App.EditModel,
template : _.template(EditTemplate),
regions : {
"HomeRegion": "#home"
}
});
return Layout;
});
And in the browser I am getting- "Uncaught ReferenceError: firstName is not defined" because the model is not being mapped correctly.
How should I handle this?
When your layout code is first seen the extend function runs taking in all your options, at this point in time I imagine App.EditModel does not yet exist and so the model is stored as undefined
If you want to pass the model to your LayoutView then you should do this when you instantiate it i.e. new Layout({model: this.model});

Does BackboneFire support non-AutoSync collection with AutoSync model

Basically I want this set up:
var Game = Backbone.Model.extend({
defaults: {
},
autoSync: true
});
var Games = Backbone.Firebase.Collection.extend({
url: 'https://<myapp>.firebaseio.com/games',
autoSync: false,
model: Game
});
Each Game should be auto-synced with server data but I do not not want to listen to the entire child_* Firebase family of events. The goal is to update individual item view instead of repainting the whole list.
Kind regards to folks out there and happy coding ;)
You can update individual items by using an Backbone.Firebase.Collection with autoSync enabled. To do re-render individual items you need to listen to when the items fire the change event. This concept is shown in the BackboneFire Quickstart in the Firebase docs.
A quick note however, you cannot mix a Backbone.Firebase.Model with a Backbone.Firebase.Collection.
Todo Model & Collection
In the below sample, notice how a regular Backbone.Model is being used in the Backbone.Firebase.Collection. The collection by default has autoSync enabled.
// A simple todo model
var Todo = Backbone.Model.extend({
defaults: { title: "New Todo" }
});
// Create a Firebase collection and set the 'firebase' property
// to the URL of your Firebase
var TodoCollection = Backbone.Firebase.Collection.extend({
model: Todo,
url: "https://<your-firebase>.firebaseio.com"
});
Todo View
The below sample is a view for an individual todo item. Inside of the initialize function the listenTo method is used to listen to a model's change event. The change event will be fired each time the model is updated either remotely or locally (which persists changes remotely).
// A view for an individual todo item
var TodoView = Backbone.View.extend({
tagName: "li",
template: _.template("<%= title %>"),
initialize: function() {
// whenever the model changes trigger a re-render of the single view
this.listenTo(this.model, "change", this.render);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
});
Render List
With the TodoView set up the list can easily be rendered as well. In the initialize function of the AppView below, we listen to the collection which will be a TodoCollection. Whenever an item is added to the collection the addOne function is executed. The addOne function simply appends a new TodoView to the page.
// The view for the entire application
var AppView = Backbone.View.extend({
el: $('#todoapp'),
initialize: function() {
this.list = this.$("#todo-list"); // the list to append to
// by listening to when the collection changes we
// can add new items in realtime
this.listenTo(this.collection, 'add', this.addOne);
},
addOne: function(todo) {
var view = new TodoView({model: todo});
this.list.append(view.render().el);
}
});

backbone: initialize model subclasses with additional attributes

I'm new to backbone, so please bear with me. I would like to create a collection in which the models all have a handful of critical attributes which they share as well as a number of other attributes which they do not share. I thought the best way to do this would be to extend a superclass model (containing defaults for all of the shared attributes) such that when I instantiate a new subclass model, those attributes are initialized and additional attributes specific to the subclass are also added to the model. I don't know how to accomplish this, but here is the direction I've been working in:
app.Fruit = Backbone.Model.extend(
{
defaults: {
name: "none",
classification: "none",
color: "none"
},
initialize: function()
{
console.log("Fruit Initialized");
}
});
app.Apple = app.Fruit.extend(
{
url: "/php/Apple.php",
initialize: function()
{
console.log("Apple initialized");
// somehow fetch additional information from server
// and add sublcass-specific attributes to model
// (for example, in the case of an apple, an attribute called cultivar)
}
});
var FruitCollection = Backbone.Collection.extend(
{
model: function(attributes, options)
{
switch(attributes.name)
{
case "Apple":
return new app.Apple(attributes, options);
break;
default:
return new app.Fruit(attributes, options);
break;
}
}
// ...
});
app.fruitCollectionCurrent = new FruitCollection([
{name: "Apple"},
{name: "Orange"}
]);
// logs: Fruit Initialized
Any suggestions on how to properly initialize a subclass with additional attributes would be appreciated.
Thanks.
EDIT: THE SOLUTION
I thought I would post the code that ended up working for me... Arrived at it thanks to #Mohsen:
app.Fruit = Backbone.Model.extend(
{
defaults: {
name: "none",
classification: "none",
color: "none"
},
initialize: function()
{
console.log("Fruit Initialized");
}
});
app.Apple = app.Fruit.extend(
{
url: "/php/Apple.php",
initialize: function()
{
console.log("Apple initialized");
return this.fetch();
}
});
I didn't even really need the asynchronous call in the subclass because I wasn't fetching any additional data for Fruit (Fruit's attributes were just set in the constructor), only for Apple. What I was really looking for was the call to this.fetch() with the specified URL. Sorry if the question made things seem more complex...
Backbone is not easy to work with when it comes to hierarchy. I solve this problem by calling parent model/collection initializer inside of my child model/collection initializer.
Fruit = Backbone.Model.extend({
url: "/fruits:id",
initialize: function initialize () {
this.set('isWashed', false);
return this.fetch();
}
});
Apple = Fruit.extend({
url: "/fruits/apples:id"
initialize: function initialize () {
var that = this;
Fruit.prototype.initialize.call(this, arguments).then(function(){
that.fetch();
})
this.set("hasSeed", true);
}
});
Now your Apple model does have all properties of a Fruit.
Key line is Fruit.prototype.initialize.call(this, arguments);. You call initialize method of Fruit for Apple.
You can also use this.__super__ to access parent model:
this.__super__.prototype.initialize.call(this, arguments);

Using Backbone.js Models/Collections with static JSON

I'm trying to learn Backbone by diving right in and building out a simple "question" app, but I've been banging my head against the wall trying to figure out how to use models and/or collections correctly. I've added the code up to where I've gotten myself lost. I'm able to get the collection to pull in the JSON file (doing "var list = new QuestionList; list.getByCid('c0') seems to return the first question), but I can't figure out how to update the model with that, use the current model for the view's data, then how to update the model with the next question when a "next" button is clicked.
What I'm trying to get here is a simple app that pulls up the JSON on load, displays the first question, then shows the next question when the button is pressed.
Could anyone help me connect the dots?
/questions.json
[
{
questionName: 'location',
question: 'Where are you from?',
inputType: 'text'
},
{
questionName: 'age',
question: 'How old are you?',
inputType: 'text'
},
{
questionName: 'search',
question: 'Which search engine do you use?'
inputType: 'select',
options: {
google: 'Google',
bing: 'Bing',
yahoo: 'Yahoo'
}
}
]
/app.js
var Question = Backbone.Model.Extend({});
var QuestionList = Backbone.Collection.extend({
model: Question,
url: "/questions.json"
});
var QuestionView = Backbone.View.extend({
template: _.template($('#question').html()),
events: {
"click .next" : "showNextQuestion"
},
showNextQuestion: function() {
// Not sure what to put here?
},
render: function () {
var placeholders = {
question: this.model.question, //Guessing this would be it once the model updates
}
$(this.el).html(this.template, placeholders));
return this;
}
});
As is evident, in the current setup, the view needs access to a greater scope than just its single model. Two possible approaches here, that I can see.
1) Pass the collection (using new QuestionView({ collection: theCollection })) rather than the model to QuestionView. Maintain an index, which you increment and re-render on the click event. This should look something like:
var QuestionView = Backbone.View.extend({
initialize: function() {
// make "this" context the current view, when these methods are called
_.bindAll(this, "showNextQuestion", "render");
this.currentIndex = 0;
this.render();
}
showNextQuestion: function() {
this.currentIndex ++;
if (this.currentIndex < this.collection.length) {
this.render();
}
},
render: function () {
$(this.el).html(this.template(this.collection.at(this.currentIndex) ));
}
});
2) Set up a Router and call router.navigate("questions/" + index, {trigger: true}) on the click event. Something like this:
var questionView = new QuestionView( { collection: myCollection });
var router = Backbone.Router.extend({
routes: {
"question/:id": "question"
},
question: function(id) {
questionView.currentIndex = id;
questionView.render();
}
});

Backbone.js - View not reloading

So I've managed to figure out how to populate my collection from an external file, and render a view based on a url, but I'm running into a problem. The code below is working as intended, except on page load, I'm getting the following error:
Uncaught TypeError: Cannot call method 'get' of undefined
Getting rid of "view.render()" eliminates the error, but now the app no longer responds to ID changes in the url (e.g. going from #/donuts/1 to #/donuts/2 does not update the view)
Could someone point me in the right direction here?
The Code:
(function(){
var Donut = Backbone.Model.extend({
defaults: {
name: null,
sprinkles: null,
cream_filled: null
}
});
var Donuts = Backbone.Collection.extend({
url: 'json.json',
model: Donut,
initialize: function() {
this.fetch();
}
})
var donuts = new Donuts();
var donutView = Backbone.View.extend({
initialize: function() {
this.collection.bind("reset", this.render, this)
},
render: function() {
console.log(this.collection.models[this.id].get('name'))
}
});
var App = Backbone.Router.extend({
routes: {
"donut/:id" : 'donutName',
},
donutName: function(id) {
var view = new donutView({
collection: donuts,
id: id
});
view.render();
}
});
var app = new App();
Backbone.history.start();
})(jQuery);
The JSON:
[
{
"name": "Boston Cream",
"sprinkles" : "false",
"cream_filled": "true"
},
{
"name": "Plain",
"sprinkles": "false",
"cream_filled": "false"
},
{
"name": "Sprinkles",
"sprinkles": "true",
"cream_filled": "false"
}
]
Looks like a bit of a flow issue here. You have the view listening to the collection's "reset" event. So when there's a reset, the view will render. That is just fine. But I believe the problem is in your router. When you route, you're creating a new instance of the view, but not doing anything with the collection, so its state is the same.
Since you're already observing the collection, do nothing with the view. When you route, update the collection's url, then do a fetch. This will trigger a reset and the view should then update itself.

Categories