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">
All Posts<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"]}
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 });
},
Related
In my App i have created a View. this View is composed of a Template like a little Form. The Form has an button and in my View i create an click event to handle this button to create a new instance of another View passing the Form data to this View and put the data on html element. The problem is: if i enter in home route or in product 3 times and send a Form data, will appears 3 same Form datas.
Form view
window.userFormView = Backbone.View.extend({
el:$("#principal"),
events : {
'click .userButton' : 'newUser'
},
initialize:function(){
this.template = _.template($("#userFormView").html());
},
newUser : function(ev) {
ev.preventDefault();
//criamos uma nova instancia do model
window.user_view = new userViewes({model: users});
var u = { nome : $("#iName").val() ,sobrenome : $("#iLName").val() };
var user = new userModel(u);
users.add(user);
console.log(users);
return false;
},
render: function() {
this.$el.html("");
this.$el.html(this.template);
}
});
Form Template View
<script type="text/template" id="userFormView">
<form action="" id="form-new-user" class="formulario">
<span class="label">Name?</span><input type="text" id="iName" class="input">
<span class="label">Last Name?</span><input type="text" id="iLName" class="input">
<button class="userButton">Send</button>
<hr>
</form>
</script>
and my route are like this:
window.AppRouter = Backbone.Router.extend({
//
// Definindo rotas
//
routes: {
'home': 'index',
'product': 'productsList',
'foo1': 'doNothing1',
'foo2': 'doNothing2'
},
index: function () {
window.users = new userCollections();
window.userForm = new userFormView();
},
productsList : function() {
window.pCollection = new productCollections();
window.produtoForm = new produtoFormView();
},
doNothing1: function () {
console.log('doNothing1()');
},
doNothing2: function () {
console.log('doNothing2()');
}
});
window.router = new AppRouter();
Backbone.history.start();
userViewes view
window.userViewes = Backbone.View.extend({
// model: users,
el: $("#userContainer"),
initialize: function(){
this.model.on("add", this.render, this);
this.model.on("remove", this.render, this);
},
render: function() {
var self = this;
self.$el.html("");
this.model.each(function(user, indice) {
self.$el.append((new userView({model: user })).render().$el);
});
return this;
}
});
and finally userView:
window.userView = Backbone.View.extend({
//model: new userModel(),
tagName : 'div',
class : "userName",
events :{
'click .editar' : 'editar',
'click .remover' : 'remover',
'blur .sobrenome': 'fechar',
'keypress .sobrenome' : 'onEnterUpdate',
},
editar : function(ev) {
ev.preventDefault();
this.$('.sobrenome').attr('contenteditable', true).focus();
},
fechar : function(ev) {
var sobrenome = $(".sobrenome").text();
this.model.set("sobrenome", sobrenome);
$(".sobrenome").val();
this.$(".sobrenome").removeAttr("contenteditable");
},
onEnterUpdate : function(ev) {
var self = this;
if(ev.keyCode == 13) {
self.fechar();
_.delay(function(){
self.$(".sobrenome").blur();
}, 100);
}
},
remover : function(ev) {
ev.preventDefault();
window.users.remove(this.model);
},
initialize: function(){
this.template = _.template($("#userTemplate").html());
},
render : function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
When your view is using el option, make sure you clean up the existing view before you make a new one.
As it is, every time you switch between routes (without a full page refresh) a new instance pointing to same element is created which causes more and more event handlers to be bound to the el element which is in DOM, and the views stay in memory because of the binding. Try something like:
index: function () {
window.users = window.users || new userCollections();
if(window.userForm){
// clean up is important
window.userForm.remove();
}
window.userForm = new userFormView();
},
And of course, instead of repeating similar code in all routes, have a variable like this.currentView that points to the active view, and a common function that does necessary clean up
P.S: Adding properties to window object is a bad practice. Create your own name space or use the Router instance instead of window
I have found the answer. i implemented singleton pattern to get only one instance of the object. follow the code:
var single = (function(){
function createInstance() {
window.userForm = new userFormView();
window.users = new userCollections();
}
function users() {
return window.users;
}
function userForm() {
return window.userForm;
}
return {
init : function() {
if(!window.users && !window.userForm) {
createInstance();
}else{
this.render();
}
},
render: function() {
window.userForm.render();
}
}
}());
single.init();
MyView.js:
define(['app/models/MyModel'],
function (MyModel) {
return Mn.LayoutView.extend({
template: '#my-template',
className: 'my-classname',
regions: {
content: '.content-region',
panel: '.panel-region'
}
initialize: function () {
_.bindAll(this, 'childButtonClicked');
},
onShow: function () {
this.getRegion('content').show(new AnotherView());
},
childEvents: {
'some-child-click': 'childButtonClicked'
},
childButtonClicked: function (view) {
var newView = new MyView({
model: new MyModel({
title: view.model.get('title')
})
});
this.getRegion('panel').show(newView);
}
});
});
I'm trying to nest instances of MyView within itself. This worked correctly when I was building the prototype by dumping everything into one function, like so:
var MyView = Mn.LayoutView.extend({
...
childButtonClicked: function(view) {
var newView = new MyView({
...
Now that I'm trying to separate the Views into their own files and use require.js, I can't figure out the syntax for a self-referential view.
When I run this code as is, I get an error like 'MyView is undefined'.
If I add it to the require header like so:
define(['app/models/MyModel', 'app/views/MyView'],
function (MyModel, MyView) {
I get the error 'MyView is not a function'.
EDIT for solution:
The marked solution works fine, I ended up using the obvious-in-hindslght:
define(['app/models/MyModel'],
function (MyModel) {
var MyView = Mn.LayoutView.extend({
template: '#my-template',
className: 'my-classname',
regions: {
content: '.content-region',
panel: '.panel-region'
}
initialize: function () {
_.bindAll(this, 'childButtonClicked');
},
onShow: function () {
this.getRegion('content').show(new AnotherView());
},
childEvents: {
'some-child-click': 'childButtonClicked'
},
childButtonClicked: function (view) {
var newView = new MyView({
model: new MyModel({
title: view.model.get('title')
})
});
this.getRegion('panel').show(newView);
}
});
return MyView;
});
You can require() in your module: var MyView = require(app/views/MyView);.
So for want of a better place:
childButtonClicked: function (view) {
var MyView = require(app/views/MyView);
var newView = new MyView({
model: new MyModel({
title: view.model.get('title')
})
});
this.getRegion('panel').show(newView);
}
I somehow made basic to-do list, but without ability to save tasks on a server. Now started implementing server side, but having a hard time grasping the whole idea of how is that all supposed to communicate with each other. Here's what I tried to do: in a TodoView (which is rendering the list) I made a model with input value and via save() method tried to post in on a server. As i said I lack grasping a general idea, how is that json supposed to be saved on a server (will it be saved in a tasks.json file which I passed in Collection?). Any help, tips and directions would be greatly appreciated!
Here's my Backbone code
var Model = Backbone.Model.extend({
default: {
task: '',
completed: false
}
});
var Collection = Backbone.Collection.extend({
model: Model,
url: '/tasks.json'
});
var ItemView = Backbone.View.extend({
tagName: 'li',
render: function () {
this.$el.data('cid', this.model.cid);
this.$el.html(this.model.toJSON().task);
return this;
}
});
var TodoView = Backbone.View.extend({
el: '#todo',
initialize: function () {
this.collection = new Collection();
this.collection.on('add', this.render, this);
// this.collection.fetch();
},
events: {
'click .add': 'add',
'dblclick li': 'destroy',
'keydown': 'keyEvent'
},
add: function () {
// this.collection.add(new Model({ //adding input as an model to collection
// task: this.$('#todo').val(),
// completed: false
// }));
var taskData = {
task: this.$('#todo').val(),
completed: false
};
var newlyCreatedModel = new Model({
task: this.$('#todo').val(),
completed: false
});
newlyCreatedModel.save({
// url: '/tasks.json',
data: JSON.stringify(taskData),
type: 'POST',
contentType: 'application/json'
});
this.$('#todo').val(''); //clearing input field
this.$('#todo').focus(); //focusing input after adding task
},
keyEvent: function (e) {
if (e.which === 13) {
this.add();
}
},
destroy: function (e) {
// e.preventDefault();
var id = $(e.target).data('cid');
var model = this.collection.get(id);
model.destroy();
e.target.remove();
},
render: function (newModel) {
var self = this,
todoView;
todoView = new ItemView({
model: newModel
});
self.$('.list').append(todoView.render().el);
return this;
}
});
var trigger = new TodoView();
Here's my html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Validation</title>
</head>
<body>
<div id="todo">
<label for="todo">Write your task</label>
<input type="text" id='todo'><button class='add'>add</button>
<ul class='list'>
</ul>
</div>
<script src="jquery.js"></script>
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="main.js"></script>
</body>
</html>
and finally here's my server written on node.js with help of express.js
var express = require('express');
var server = express();
server.use(express.static(__dirname + '/test'));
server.post('/somepath', function (req, res) {
var bodyStr = '';
req.on('data', function (chunk) {
bodyStr += chunk.toString();
});
req.on('end', function (chunk) {
// res.send(bodyStr);
console.log(bodyStr);
});
});
var server = server.listen(8888);
I am getting this error . I am able to preform read, and remove functions using BackboneJs , but i am having error when i execute the add method any help will be appreciated.
JSfiddel path is http://jsfiddle.net/2wjdcgky/
BackboneJS Uncaught Error: A "url" property or function must be specified
$(function() {
Model
var modelContact = Backbone.Model.extend({
defaults: function() {
return {
Id: 0,
Name: "",
Address: ""
};
},
idAttribute: "Id"
});
ModelCollection
var contactCollection = Backbone.Collection.extend({
model: modelContact,
url: function() {
return 'api/Contact';
},
add: function(model) {
this.sync("create", model); // Error On create
},
remove: function(model) {
this.sync("delete", model); //Runs Fine
}
});
var contacts = new contactCollection;
View
var contactView = Backbone.View.extend({
tagName: "tr",
events: {
"click a.destroy": "clear"
},
template: _.template($("#newContacttemplate").html()),
initialize: function() {
this.model.on("change", this.render, this);
this.model.on('destroy', this.remove, this);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
clear: function(e) {
contacts.remove(this.model); // runs fine
}
});
Main View
var main = Backbone.View.extend({
el: $("#contactApp"),
events: {
"click #btnsave": "CreateNewContact"
},
initialize: function() {
this.Nameinput = this.$("#contactname");
this.Addressinput = this.$("#contactaddress");
contacts.on("add", this.AddContact, this);
contacts.on("reset", this.AddContacts, this);
contacts.fetch();
},
AddContact: function (contact) {
console.log("AddContact");
var view = new contactView({ model: contact });
this.$("#tblcontact tbody").append(view.render().el);
},
AddContacts: function () {
console.log("AddContacts");
contacts.each(this.AddContact);
},
CreateNewContact: function (e) {
console.log(e);
//Generate an error "BackboneJS Uncaught Error: A "url" property or function must be specified"
contacts.add({ Name: this.Nameinput.val(), Address: this.Addressinput.val() });
}
});
var m = new main;
});
Your JSFiddle was missing Backbone references and all.
Working update: http://jsfiddle.net/apt7hchL/2/
Much simpler code (no need to define those add and remove methods on the collection!). Also more common Javascript coding style conventions.
Please note I had to manually generate an "Id" attribute to allow creating more than one contact. As you are making Id = 0 by default, second model with same is not added, as Backbone sees a model with id=0 is already in the collection.
When you want to save, call the model.save() method. Don't call sync manually, you'll normally don't need to!
For the model to be saved to the database before being added to the collection, use:
createNewContact: function (e) {
e.preventDefault();
var self = this;
var newContact = new ContactModel({
Name: this.$("#name").val(),
Address: this.$("#address").val()
});
newContact.save({ success: function(model){
self.collection.add(model);
});
//clear form
this.$("#name").val("");
this.$("#address").val("");
}
Sync method tries to sync to a server setup to handle it, with CRUD abilities. If thats not what you're looking for, and you just want to display this information on the client side, instead of using sync, you should use Collection.add(model) and Collection.remove(model)
Now Solved - See bottom....
I've got a Backbone list view with a button on it that should show the edit elements.
Neither the jQuery hide() call in the 'showAddEntry' function or the view rendering for 'versionEditView' are doing anything at all. I've stepped right through and I'm not getting any errors. I've even tried manually running methods in the console to see what's going on with hide, but I'm not getting anywhere.
Here's the main view...
define(['ministry', 'jquery', 'models/m-version-info', 'views/about/v-edit-version-info-entry', 'text!templates/version-info/version-info.html'],
function(Ministry, $, VersionInfo, VersionInfoEditView, TemplateSource) {
var versionInfoEntriesView = Ministry.View.extend({
el: '#mainAppArea',
template: Handlebars.compile(TemplateSource),
versionInfoEditView: null,
initialize: function () {
this.$addEntryArea = $('#addVersionInfoEntryArea');
this.$addEntryButton = $('#addVersionInfoEntryButton');
},
events: {
'click #addVersionInfoEntryButton': 'showAddEntry'
},
render: function () {
var that = this;
var entries = new VersionInfo.Collection();
entries.fetch({
success: function (data) {
that.$el.html(that.template({ items: data.toJSON() }));
}
});
return this;
},
showAddEntry: function() {
if (this.versionInfoEditView != null) {
this.versionInfoEditView.trash();
}
this.versionInfoEditView = new VersionInfoEditView({ el: this.$addEntryArea });
this.$addEntryButton.hide();
this.versionInfoEditView.render();
return false;
}
});
return versionInfoEntriesView;
});
And here's the child view...
define(['ministry', 'models/m-version-info', 'text!templates/version-info/edit-version-info- entry.html', 'jquery.custom'],
function (Ministry, VersionInfo, TemplateSource) {
var editVersionInfoView = Ministry.View.extend({
template: Handlebars.compile(TemplateSource),
initialize: function () {
this.$dbVersionInput = this.$('#dbVersion');
this.$tagInput = this.$('#tag');
},
render: function () {
this.$el.html(this.template());
return this;
},
events: {
'submit .edit-version-info-form': 'saveEntry'
},
saveEntry: function() {
var entry = new VersionInfo.Model({ dbVersion: this.$dbVersionInput.val(), tag: this.$tagInput.val() });
entry.save({
success: function() {
alert('Your item has been saved');
}
});
return false;
}
});
return editVersionInfoView;
});
And the main template...
<h2>Version Info</h2>
<div id="info">
<a id="addVersionInfoEntryButton" href="#/versioninfo">Add manual entry</a>
<div id="addVersionInfoEntryArea">
</div>
<ul id="items">
{{#each items}}
<li>{{dbVersion}} | {{tag}}</li>
{{/each}}
</ul>
</div>
And the edit template...
<form class="edit-version-info-form">
<h3>Create a new entry</h3>
<label for="dbVersion">DB Version</label>
<input type="text" id="dbVersion" maxlength="10" />
<label for="tag">Tag</label>
<input type="text" id="tag" />
<button type="submit" id="newEntryButton">Create</button>
</form>
I'm fairly new to backbone so I may well be doing something totally wrong, but I can't see anything wrong with the approach so far and it's not throwing any errors.
OK - Fix as follows after some facepalming...
define(['ministry', 'jquery', 'models/m-version-info', 'views/about/v-edit-version-info-entry', 'text!templates/version-info/version-info.html'],
function(Ministry, $, VersionInfo, VersionInfoEditView, TemplateSource) {
var versionInfoEntriesView = Ministry.View.extend({
el: '#mainAppArea',
template: Handlebars.compile(TemplateSource),
versionInfoEditView: null,
$addEntryArea: undefined,
$addEntryButton: undefined,
initialize: function () {
},
events: {
'click #addVersionInfoEntryButton': 'showAddEntry'
},
render: function () {
var that = this;
var entries = new VersionInfo.Collection();
entries.fetch({
success: function (data) {
that.$el.html(that.template({ items: data.toJSON() }));
that.$addEntryArea = that.$('#addVersionInfoEntryArea');
that.$addEntryButton = that.$('#addVersionInfoEntryButton');
}
});
return this;
},
showAddEntry: function (e) {
e.preventDefault();
if (this.versionInfoEditView != null) {
this.versionInfoEditView.trash();
}
this.versionInfoEditView = new VersionInfoEditView({ el: this.$addEntryArea });
this.$addEntryButton.hide();
this.$addEntryArea.append('Do I want to put it here?');
this.versionInfoEditView.render();
}
});
return versionInfoEntriesView;
});
The issue was due to the fact that I was setting the internal element variables within the view before the completion of the render, so the elements were linked up to nothing. I resolved this by extracting the element initiation to the end of the render success callback.
Here's the fix again...
define(['ministry', 'jquery', 'models/m-version-info', 'views/about/v-edit-version-info-entry', 'text!templates/version-info/version-info.html'],
function(Ministry, $, VersionInfo, VersionInfoEditView, TemplateSource) {
var versionInfoEntriesView = Ministry.View.extend({
el: '#mainAppArea',
template: Handlebars.compile(TemplateSource),
versionInfoEditView: null,
$addEntryArea: undefined,
$addEntryButton: undefined,
initialize: function () {
},
events: {
'click #addVersionInfoEntryButton': 'showAddEntry'
},
render: function () {
var that = this;
var entries = new VersionInfo.Collection();
entries.fetch({
success: function (data) {
that.$el.html(that.template({ items: data.toJSON() }));
that.$addEntryArea = that.$('#addVersionInfoEntryArea');
that.$addEntryButton = that.$('#addVersionInfoEntryButton');
}
});
return this;
},
showAddEntry: function (e) {
e.preventDefault();
if (this.versionInfoEditView != null) {
this.versionInfoEditView.trash();
}
this.versionInfoEditView = new VersionInfoEditView({ el: this.$addEntryArea });
this.$addEntryButton.hide();
this.$addEntryArea.append('Do I want to put it here?');
this.versionInfoEditView.render();
}
});
return versionInfoEntriesView;
});
The issue was due to the fact that I was setting the internal element variables within the view before the completion of the render, so the elements were linked up to nothing. I resolved this by extracting the element initiation to the end of the render success callback.