2

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

tchaymore
  • 3,728
  • 13
  • 55
  • 86

1 Answers1

5

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();

});
satchmorun
  • 12,487
  • 2
  • 41
  • 27
  • So I tried to follow your advice about creating a separate view for each item in the list . . . and I broke the app. You can see my attempt here: http://jsbin.com/emicaw/3. Any advice? Happy to open a view question. – tchaymore Dec 30 '11 at 04:09
  • I cloned you again, deleted all the old comments, and added some new ones. http://jsbin.com/oviqif/2/edit. I think you're pretty close, there's just some nooks and crannies of how backbone works that you need to settle in on. Me too, for that matter. I'd recommend reading through the source (http://documentcloud.github.com/backbone/docs/backbone.html). 1: it's pretty. 2: it's well-written (code and prose) 3: it'll give you a good idea of what's going on under the hood, so to speak. – satchmorun Dec 30 '11 at 05:18