1

I'm writing my first Backbone blog app but when i try to add new post it throws an error.

Here is my app.js (all of backbone related components are in this file):

_.templateSettings = {
    interpolate: /\{\{(.+?)\}\}/g
};

var Post = Backbone.Model.extend({});
var Posts = Backbone.Collection.extend({
    model : Post,
    url : "/posts"
});

var PostListView = Backbone.View.extend({
    tagName: "li",
    template: _.template("<a href='/posts/{{id}}'>{{title}}</a>"),
    events: {
        'click a': 'handleClick'
    },
    handleClick: function (e) {
        e.preventDefault();
        postRouter.navigate($(e.currentTarget).attr("href"),
            {trigger: true});
    },
    render: function () {
        this.el.innerHTML = this.template(this.model.toJSON());
        return this;
    }
});

var PostsListView = Backbone.View.extend({
    template: _.template("<h1>My Blog</h1><a href='/post/new' class='newPost'>New</a> <ul></ul>"),
    events: {
        'click .newPost': 'handleNewClick'
    },
    handleNewClick: function (e) {
        e.preventDefault();
        postRouter.navigate($(e.currentTarget).attr("href"),
            {trigger: true});
    },
    render: function () {
        this.el.innerHTML = this.template();
        var ul = this.$el.find("ul");
        this.collection.forEach(function (post) {
            ul.append(new PostListView({
                model: post
            }).render().el);
        });
        return this;
    }
});

var PostView = Backbone.View.extend({
    template: _.template($("#postView").html()),
    events: {
        'click a': 'handleClick'
    },
    render: function () {
        var model = this.model.toJSON();
        model.pubDate = new Date(Date.parse(model.pubDate)).toDateString();
        this.el.innerHTML = this.template(model);
        return this;
    },
    handleClick: function (e) {
        e.preventDefault();
        postRouter.navigate($(e.currentTarget).attr("href"),
            {trigger: true});
        return false;
    }
});

var PostFormView = Backbone.View.extend({
    tagName: 'form',
    template: _.template($("#postFormView").html()),
    initialize: function (options) {
        this.posts = options.posts;
    },
    events: {
        'click #submitPost': 'createPost',
        'click .back' : 'backButton'
    },
    render: function () {
        this.el.innerHTML = this.template();
        return this;
    },
    backButton: function (e) {
        e.preventDefault();
        postRouter.navigate($(e.currentTarget).attr("href"),
            {trigger: true});
        return false;
    },
    createPost: function (e) {
        e.preventDefault();
        var postAttrs = {
            content: $("#postText").val(),
            title: $("#postTitle").val(),
            pubDate: new Date(),
        };
        this.posts.create(postAttrs);
        postRouter.navigate("/", { trigger: true });
        return false;
    }
});

var PostRouter = Backbone.Router.extend({
    initialize: function (options) {
        this.posts = options.posts;
        this.main  = options.main;
    },
    routes: {
        '': 'index',
        'posts/:id': 'singlePost',
        'post/new': 'newPost'
    },
    index: function () {
        var pv = new PostsListView({ collection: this.posts });
        this.main.html(pv.render().el);
    },
    singlePost: function (id) {
        var post = this.posts.get(id);
        var pv = new PostView({ model: post });
        this.main.html(pv.render().el);
    },
    newPost: function () {
        var pfv = new PostFormView({ posts: this.posts });
        this.main.html(pfv.render().el);
    }
});

I also have some view templates in my index file :

<!DOCTYPE html>
<html>
<head>
    <title> Simple Blog </title>
</head>
<body>
<div id="main"></div>
<script src="/jquery.js"></script>
<script src="/underscore.js"></script>
<script src="/backbone.js"></script>
<script type="text/template" id="postFormView">
    <a href="/" class="back">All Posts</a><br />
    <input type="text" id="postTitle" placeholder="post title" />
    <br />
    <textarea id="postText"></textarea>
    <br />
    <button id="submitPost"> Post </button>
</script>
<script type="text/template" id="postView">
    <a href='/'>All Posts</a>
    <h1>{{title}}</h1>
    <p>{{pubDate}}</p>
    {{content}}
</script>
<script src="/app.js"></script>
<script>
    var postRouter = new PostRouter({
        posts: new Posts(<%- posts %>),
        main: $("#main")
    });
    Backbone.history.start({pushState: true});
</script>
</body>
</html>

Viewing posts and home page works fine but when I try to create a new post I get this error from the dev tools console:

Uncaught ReferenceError: id is not defined
    at child.eval (eval at _.template (http://localhost:3000/underscore.js:1:1), <anonymous>:6:8)
    at child.template (http://localhost:3000/underscore.js:1214:21)
    at child.render (http://localhost:3000/app.js:27:34)
    at http://localhost:3000/app.js:48:16
    at Array.forEach (native)
    at Function._.each._.forEach (http://localhost:3000/underscore.js:79:11)
    at child.Collection.(anonymous function) [as forEach] (http://localhost:3000/backbone.js:956:24)
    at child.render (http://localhost:3000/app.js:45:25)
    at child.index (http://localhost:3000/app.js:118:27)
    at Object.callback (http://localhost:3000/backbone.js:1242:30)

The server is a simple nodejs server and output for creating a post is something like this:

{"result":{"ok":1,"n":1},"ops":[{"content":"kljhlkjh","title":"jkhjklh","pubDate":"2016-10-29T10:21:47.793Z","id":12,"_id":"5814783b732bbe153461eca4"}],"insertedCount":1,"insertedId
s":["5814783b732bbe153461eca4"]}
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
AminSojoudi
  • 1,904
  • 2
  • 18
  • 42

1 Answers1

4

Where is the error?

First, you need to find what is causing the error, and where does it comes from.

The error comes from the following line inside the PostListView's render function:

this.el.innerHTML = this.template(this.model.toJSON());

And the error is thrown by underscore's template rendering. Which comes down to:

template: _.template("<a href='/posts/{{id}}'>{{title}}</a>"),

See the {{id}}? It's that one that is not defined when the error occurs.

What "{{id}} not defined" means?

You're passing this.model.toJSON() as the data for the template. So, it means that this.model's id attribute is not defined yet.

Why is my model id not defined yet?

It's because creating a new model through the collection's create function is asynchronous.

this.posts.create(postAttrs);
// the model hasn't received an id from the server yet.
postRouter.navigate("/", { trigger: true });

How to wait after a collection's create call?

Backbone offers success and error callbacks for most (if not all) its asynchronous functions.

The options hash takes success and error callbacks which will both be passed (collection, response, options) as arguments.

So, you could change your createPost function to the following, adding a onPostCreated callback.

createPost: function(e) {
    e.preventDefault();
    var postAttrs = {
        content: $("#postText").val(),
        title: $("#postTitle").val(),
        pubDate: new Date(),
    };
    this.posts.create(postAttrs, {
        context: this,
        success: this.onPostCreated
    });
    return false;
},
onPostCreated: function() {
    // you don't need to use the router, so your views are self-contained
    Backbone.history.navigate("/", { trigger: true });
},
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129