2

I'm learning backbone.js and have a problem following this tutorial. I have a Collection of Eras called History. When trying app.History.create({ from: 0, until: 1, stash: {from: null, until: null}, _enabled: true}) I get the error in the title.

Here's my code:

The Model

var app = app || {};

app.Era = Backbone.Model.extend({

    defaults: {
        from: Number.NEGATIVE_INFINITY,
        until: Number.POSITIVE_INFINITY,
        stash: {
            from: null,
            until: null
        },
        _enabled: true
    },

    toggle: function(){
        if(this.get('_enabled')){
            this.disable();
        }else{
            this.enable();
        }

        this.save();
    },

    enable: function(){
        this.from = this.stash.from;
        this.until = this.stash.until;

        this.stash.from = null; // strictly speaking unnecssary
        this.stash.until = null;

        this._enabled = true;
    },

    disable: function(){
        this.stash.from = this.from;
        this.stash.until = this.until;

        this.from = null;
        this.until = null;

        this._enabled = false;
    },

    enabled: function(){
        return this._enabled;
    },

});

var History = Backbone.Collection.extend({

    model: app.Era,

    localStorage: new Backbone.LocalStorage('karass-history'),

    enabled: function(){
        return this.filter(function(era){
            return era.enabled();
        }); 
    },

    nextOrder: function(){
        if(!this.length){
            return 1;
        }

        return this.last().get('order') + 1;
    },

    comparator: function( era ) {
        return era.get('order');
    },

    comparatorFrom: function( era ){
        return era.get('from');
    },

    comparatorUntil: function(era){
        return era.get('until');
    },

    getFirstFrom: function(){
        // todo
    },

    getLastUntil: function(){
        // todo
    },

    getEraAt: function(time){
        // todo
    },

});

app.History = new History();

The Views

app.AppView = Backbone.View.extend({

    // bind to the karassApp div we set up in the html file
    el: "#karassApp", 

    // Our template for the line of statistics at the bottom of the app.
    statsTemplate: _.template( $('#stats-template').html() ),

    // delegated events for creating new items, and clearing completed ones
    events: {
        'keypress #new-era-start': 'focusOnEnd',
        'keypress #new-era-end': 'createEraOnEnter',
        'click #disable-history': 'toggleEnabledAllEras'
    },

    // At initialization we bind to the relevant events on the 'eras' 
    // collection, when items are added or changed. Kick things off by
    // loading any preexisting todos that might be saved in *localStorage*
    initialize: function(){
        //_.bindAll(this);

        this.era_start = this.$('#new-era-start');
        this.era_end = this.$('#new-era-end');
        this.disableHistory = this.$('#disable-history');
        this.$footer = this.$('#footer');
        this.$main = this.$('#main');

        window.app.History.on('add', this.addOneEra, this);
        window.app.History.on('reset', this.addAllEras, this);
        // window.app.History.on('add:true', this.addAllEras, this);
        window.app.History.on('change:_enabled', this.filterOneEra, this);
        window.app.History.on('filter', this.filterAllEras, this);

        window.app.History.on('all', this.renderHistory, this);

        app.History.fetch();
    },

    // Re-rendering the App just means refreshing the statistics -- the rest
    // of the app doesn't change.
    renderHistory: function(){
        var era_count = app.History.length();

        if(app.History.length){
            this.$main.show();
            this.$footer.show();

            this.$footer.html(this.statsTemplate({
                era_count: era_count,
            }));

            this.$('#filters li a')
                .removeClass('selected')
                .filter('[href=#/' + (app.EraFilter || '' ) + '"]')
                .addClass('selected');

        }else{
            this.$main.hide();
            this.$footer.hide();
        }

        this.disableHistory = !app.History.enabled();
    },

    // Add a single era item to the list by creating a view for it, and
    // appending its element to the `<ul>`.
    addOneEra: function(era){
        var view = new app.EraView({ model: era });
        $('#history').append(view.render().el);
    },

    addAllEras: function(){
        this.$('#history').html('');
        app.History.each(this.addOneEra, this);
    },

    filterOneEra: function(era){
        era.trigger('visible');
    },

    filterAllEras: function(){
        app.History.each(this.filterOne, this);
    },

    // Generate the attributes for the new Era Item
    newEraAttributes: function(){
        return {
            from: this.era_start.val().trim(), // validation logic should probably go here: make sure it's a date that can be saved
            until: this.era_end.val().trim(), // ditto
            stash: {
                from: null,
                until: null
            },
            order: app.History.nextOrder(),
            _enabled: true,
        }
    },

    focusOnEnd: function(e){
        if(e.which !== ENTER_KEY || !this.era_start.val().trim()){
            return;
        }

        this.era_end.focus();
    },

    createEraOnEnter: function(e){
        if(e.which !== ENTER_KEY || !this.era_start.val().trim()){
            return;
        }

        app.History.create(this.newEraAttributes());
        this.era_start.val('');
        this.era_end.val('');
    },

    toggleEnabledAllEras: function(){
        var enabled = !this.disableHistory.checked;

        app.History.each(function(era){
            era.toggle();
            era.save();
        });
    }
});

app.EraView = Backbone.View.extend({
    tagName: 'li',
    template: _.template( $('#era-template').html() ),

    // The DOM events specified to an item
    events: {
        'dblclick label': 'edit',
        'keypress .edit .start': 'focusOnEnd',
        'keypress .edit .end': 'updateOnEnter',
        'blur .edit': 'close',
    },

    // The EraView listens for changes to its model, re-rendering. Since there's
    // a one-to-one correspondence between an era and a EraView in this app, 
    // we set a direct reference on the model for convenience.
    initialize: function(){
        //_.bindAll(this);
        this.model.on('change', this.render, this);
    },

    // Re-renders the era item to the current state of the model and
    // updates the reference to the era's edit input within the view
    render: function(){
        this.$el.html( this.template(this.model.toJSON()));
        this.era_start = this.$('.era-start');
        this.era_end = this.$('.era-end');
        return this;
    },

    // Switch this view into editing mode, displaying the input field
    edit: function(){
        this.$el.addClass('editing');
        this.era_start.focus();
    },

    // Close the editing mode, saving changes to the era
    close: function(){
        var start = this.era_start.val().trim();
        var end = this.era_end.val().trim();

        if(start && end){
            this.model.save({from: start, until: end});
        }

        this.$el.removeClass('editing');
    },

    focusOnEnd: function(e){
        if(e.which !== ENTER_KEY || !this.era_start.val().trim()){
            return;
        }

        this.era_end.focus();
    },

    updateOnEnter: function(e){
        if(e.which !== ENTER_KEY || !this.era_end.val().trim()){
            return;
        }

        this.close();
    }
});

And last but not least:

The HTML

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>Backbone.js • HistoryMVC</title>
</head>
<body>
    <section id="karassApp">
        <header id="header">
            <h1>Karass</h1>
            <input id="new-era-start" placeholder="Since when?" autofocus>
            <input id="new-era-end" placeholder="Until when?" >
        </header>
        <section id="main">
            <input id="disable-history" type="checkbox">
            <label for="disable-history">Disable History, work on Snapshot instead</label>
            <ul id="history"></ul>
        </section>
        <footer id="footer"></footer>
    </section>
    <div id="info">
        No info here, traveller!
    </div>

    <script type="text/template" id="era-template">
        <li>
            <input class="era-start" placeholder="Since when?" value="<%= from %>">
            <input class="era-end" placeholder="Until when?" value="<%= until %>">
            <label>Edit</label>
        </li>
    </script>

    <script type="text/template" id="stats-template">
        <% if (era_count) { %>
            <p>There are <%= era_count %> eras to display</p>
        <% }else{ %>
            <p>There are no eras to display, yet.</p>
        <% } %>
    </script>

    <script src="js/lib/jquery-1.9.0.min.js"></script>
    <script src="js/lib/underscore-min.js"></script>
    <script src="js/lib/backbone.js"></script>
    <script src="js/lib/backbone.localStorage-min.js"></script>
    <script src="js/models/era.js"></script>
    <script src="js/models/history.js"></script>
    <script src="js/views/app.js"></script>
    <script src="js/views/era.js"></script>
</html>

Debug

I'm able to instantiate an Era using era = new app.Era({ from: 0, until: 1, stash: {from: null, until: null}, _enabled: true});, but calling app.History.create(...) with the same arguments throws the error.

Question What is the error here? I tried to do everything anologous to the tutorial, and have been over all parts of the code three times now to see if I would spot an error.

Thank you!

Oh yeah, and credit goes to Nyxynyx, whose question I shamelessly ripped off after not finding an answer to mine.


That's a bug in Backbone.localStorage, which isn't compatible with Backbone 0.9.10.

There's a pull request on Github, in the meantime replace Backbone.LocalStorage with this version.

4

1 回答 1

5

这是 Backbone.localStorage 中的一个错误,它与 Backbone 0.9.10 不兼容。

Github 上有一个pull request ,同时用这个 version替换 Backbone.LocalStorage 。

于 2013-01-21T22:09:03.623 回答