I have been through a number of questions already in regard to this. Some older questions with old methods do not work with the latest versions of _.js and backbone.js and I am a bit lost to tell you the truth as well.
Learning backbone.js at the moment, going well, going back to good habits with MVC etc which is great. Got lazy a bit with jQuery.
Anyway...
I been through a few things and this fiddle:
http://jsfiddle.net/sushanth009/bBtgt/1/
var postsObject = [{
"_id": "50f5f5d4014e045f000002",
"author": {
"name": "Chris Crawford",
"photo": "http://example.com/photo.jpg"
},
"status": "This is a sample message.",
"comments": [{
"_id": "5160eacbe4b020ec56a46844",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}, {
"_id": "5160eacbe4b020ec56a46845",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}]
}, {
"_id": "50f5f5d4014e045f000003",
"author": {
"name": "Brown Robert",
"photo": "http://example.com/photo.jpg"
},
"status": "This is another sample message.",
"comments": [{
"_id": "5160eacbe4b020ec56a46846",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}, {
"_id": "5160eacbe4b020ec56a46847",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}]
}];
// Comment Model
var Comment = Backbone.Model.extend({
idAttribute: '_id',
defaults: {
text: "",
author: ""
}
});
// Comments collection
var Comments = Backbone.Collection.extend({
model: Comment
});
// Author Model
var Author = Backbone.Model.extend({
defaults: {
text: "",
author: ""
}
});
// Post Model
var Post = Backbone.Model.extend({
idAttribute: '_id',
defaults: {
author: "",
status: ""
},
parse: function (resp) {
// Create a Author model on the Post Model
this.author = new Author(resp.author || null, {
parse: true
});
// Delete from the response object as the data is
// alredy available on the model
delete resp.author;
// Create a comments objecton model
// that will hold the comments collection
this.comments = new Comments(resp.comments || null, {
parse: true
});
// Delete from the response object as the data is
// alredy available on the model
delete resp.comments;
// return the response object
return resp;
}
})
// Posts Collection
var Posts = Backbone.Collection.extend({
model: Post
});
var PostsListView = Backbone.View.extend({
el: "#container",
renderPostView: function(post) {
// Create a new postView
var postView = new PostView({
model : post
});
// Append it to the container
this.$el.append(postView.el);
postView.render();
},
render: function () {
var thisView = this;
// Iterate over each post Model
_.each(this.collection.models, function (post) {
// Call the renderPostView method
thisView.renderPostView(post);
});
}
});
var PostView = Backbone.View.extend({
className: "post",
template: _.template($("#post-template").html()),
renderComments: function() {
var commentsListView = new CommentsListView({
// Comments collection on the Post Model
collection : this.model.comments,
// Pass the container to which it is to be appended
el : $('.comments', this.$el)
});
commentsListView.render();
},
render: function () {
this.$el.empty();
// Extend the object toi contain both Post attributes
// and also the author attributes
this.$el.append(this.template(_.extend(this.model.toJSON(),
this.model.author.toJSON()
)));
// Render the comments for each Post
this.renderComments();
}
});
var CommentsListView = Backbone.View.extend({
renderCommentView: function(comment) {
// Create a new CommentView
var commentView = new CommentView({
model : comment
});
// Append it to the comments ul that is part
// of the view
this.$el.append(commentView.el);
commentView.render();
},
render: function () {
var thisView = this;
// Iterate over each Comment Model
_.each(this.collection.models, function (comment) {
// Call the renderCommentView method
thisView.renderCommentView(comment);
});
}
});
var CommentView = Backbone.View.extend({
tagName: "li",
className: "comment",
template: _.template($("#comment-template").html()),
render: function () {
this.$el.empty();
this.$el.append(this.template(this.model.toJSON()));
}
});
// Create a posts collection
var posts = new Posts(postsObject, {parse: true});
// Pass it to the PostsListView
var postsListView = new PostsListView({
collection: posts
});
// Render the view
postsListView.render();
Has a great use of multiple collections reading from a JSON object.
What I have though is a JSON FILE which I will need to read from and also eventually write to, to update.
Could someone, through the fiddle help me understand how to change this from reading from an inline JSON object in the collection to having the collection read from a JSON file of data instead?
Thanks
General example using jQuery's ajax methods and PHP:
Client side:
$.getJSON("http://www.example.com/document.json", function (data) {
data.someProperty = "new value";
saveFile(data);
});
function saveFile(json) {
$.post("http://www.example.com/save-file-api.php", {data: json}, function (data) {
console.log(data);
});
}
Server side (save-file-api.php):
<?php
if ($_POST["data"]) {
if (file_put_contents("/document.json", $_POST["data"]) !== false) {
echo "File saved successfully.";
} else {
echo "Error while saving file.";
}
}
This could be done many ways. Basically, request your JSON document, fiddle around with it, then send it back to your server, which will save it.
Backbone also has an API for this, you may want to look into it but apparently they use jQuery behind the scenes.
Related
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)
I am validating model's attribute (Name), in order to make sure, customer have to input their name in the register form.
View :
define(["jquery" ,
"underscore" ,
"backbone" ,
"text!templates/CustomerTemplate.html",
"models/Customer"
],function($ , _ , Backbone, CustomerTemplate, CustomerModel){
var CustomerView = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model, 'change', this.render);
},
events : {
'submit #customerForm' : 'Customer'
},
Customer : function(e){
e.preventDefault()
var _customer = new CustomerModel({
UID: "00000000-0000-0000-0000-000000000000",
Sex: 0,
Name: $("#name").val(),
});
this.model.save(_customer,{validate: true},{
wait:true,
success:function(model, response) {
console.log('Successfully saved!');
},
error: function(model, error) {
console.log(model.toJSON());
console.log('error.responseText');
}
});
},
render : function(){
var customertemplate = _.template(CustomerTemplate);
this.$el.html(customertemplate(this.model.toJSON()));
return this;
}
});
return CustomerView;
});
Model:
define(["underscore" , "backbone"],function(_ , Backbone){
var CustomerModel = Backbone.Model.extend({
urlRoot: "myurl",
initialize : function(){
this.bind('invalid', function(model, error) {
console.log(error);
});
},
validate: function (attrs){
if ( !attrs.Name) {
return 'You must provide a name';
}
},
defaults : {
UID: "00000000-0000-0000-0000-000000000000",
Sex: 0,
Name: "",
}
});
return CustomerModel;
});
Problem : Even the attribute Name is not null, the error message in validate method still appears (You must provide a name).
Any idea what could be causing this is appreciate. Thanks.
When you call this.model.save in your CustomerView, you're passing it a new Customer model you instantiated in the previous statement. This isn't quite what you want; you either want to call _customer.save() to save the brand new model, or - more likely - you want to pass your new attributes to the existing model, and save that:
var newAttrs = {
UID: "00000000-0000-0000-0000-000000000000",
Sex: 0,
Name: $("#name").val(),
};
this.model.save(newAttrs);
When you call this.model.save(_customer, {validate: true}) in your existing code, that Customer model get passed to your validate() function. And that model doesn't have a Name attribute. It does have a Name property - you can access it via _customer.get('Name') - but you should follow the Backbone convention and presume that your validate method is getting a 'simple' JavaScript object, not a Backbone model.
I have a Backbone app where I'm attempting to populate a collection using a JSON file. I want to generate a list of "titles" from the JSON to eventually turn into a menu.
Everything is going well, except that Handlebars won't loop (each) over my collection to render the list.
The relevant view:
var MenuView = Backbone.View.extend({
template: Handlebars.compile(
'<ul>' +
'{{#each items.models}}<li>{{attributes.title}}</li>{{/each}}' +
'</ul>'
),
initialize: function () {
this.listenTo(this.collection, "reset", this.render);
},
render: function () {
this.$el.html(this.template(items));
return this;
}
});
The model and collection:
var Magazine = Backbone.Model.extend({
urlRoot:"/items",
defaults: {
id: '',
title: '',
pubDate: '1/1',
image: ''
}
});
var MagazineMenu= Backbone.Collection.extend({
comparator: 'title',
model: Magazine,
url: "/items"
});
The router:
var MagazineRouter = Backbone.Router.extend({
routes: {
"" : "listPage",
"titles/:id" : "showTitle"
},
initialize: function () {
this.magazineModel = new Magazine();
this.magazineModel.fetch();
this.magazineView = new MagazineView({
model: this.magazineModel
});
this.magazineCollection = new MagazineMenu();
this.magazineCollection.fetch();
this.menuView = new MenuView({collection: this.magazineCollection});
},
showTitle: function(id) {
this.magazineModel.set("id", id);
$("#theList").html(this.magazineView.render().el);
},
listPage : function() {
$('#theList').html(this.menuView.render().el);
}
});
var router = new MagazineRouter();
$(document).ready(function() {
Backbone.history.start();
});
And finally the JSON:
[
{
"id": "screamingzebras",
"url": "screamingzebras",
"title": "Screaming Zebras",
"pubDate": "2/1",
"image": "screamingzebras.jpg"
},
{
"id": "carousellovers",
"url": "carousellovers",
"title": "Carousel Lovers",
"pubDate": "3/1",
"image": "carousellovers.jpg"
},
{
"id": "gardenstatuary",
"url": "gardenstatuary",
"title": "Garden Statuary",
"pubDate": "4/1",
"image": "gardenstatuary.jpg"
},
{
"id": "sombreromonthly",
"url": "sombreromonthly",
"title": "Sombrero Monthly",
"pubDate": "1/1",
"image": "sombreromonthly.jpg"
}
]
When I run this in a browser, I get no errors in the console. If I console.log(this.collection) just before the call to this.$el.html(this.template(items)); in the view, I can see the collection with a models attribute that is properly populated from the JSON.
When I look at the Elements panel in Chrome dev tools, I can see that it is generating everything up to and including the <ul> tag. That leads me to believe that I'm just missing a key logic point that is getting the Handlebars each function to actually loop over the collection.
I see two problems here:
items isn't defined anywhere so your render is really saying this.template(undefined).
Even if you did have a local variable called items, your Handlebars template won't know that you've called it items so it won't know that {{#each items.models}} should iterator over it.
Presumably your items is really supposed to be the view's this.collection and your render should look more like this:
render: function () {
this.$el.html(this.template(this.collection));
return this;
}
That should solve problem 1. You can fix problem 2 in two ways:
Change the template to refer to the right thing.
Change how you call this.template so that items is associated with the right thing.
The first option would use the above render and a template that looks like this:
<ul>
{{#each models}}
<li>{{attributes.title}}</li>
{{/each}}
</ul>
The second option would leave your template alone but change render to use:
this.$el.html(
this.template({
items: this.collection
})
);
Another option would be to use this.collection.toJSON() to supply data to the template, then render would use:
this.$el.html(
this.template({
items: this.collection.toJSON()
})
);
and then template would be:
<ul>
{{#each items}}
<li>{{title}}</li>
{{/each}}
</ul>
I'm struggling to get a simple master-detail scenario working with Backbone. Here's the jsfiddle and code is below.
Problem 1: this navigation doesn't work at all if I switch "pushstate" to true. What I really want is to have no hashes/pound signs in my urls.
Problem 2: my users might rock up on a url like /accommodation/287, not always on the home page. How would you deal with that using the router?
Thanks a lot for any help!
var AccommodationItem = Backbone.Model.extend({
defaults: {
html: "",
loaded: false
},
urlRoot: "/Home/Accommodation/"
});
var AccommodationItemView = Backbone.View.extend({
tagName: "li",
template: _.template("<a href='#accommodation/<%= id %>'><%= description %></a>"),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var AccommodationList = Backbone.Collection.extend({
model: AccommodationItem
});
var DetailView = Backbone.View.extend({
initialize: function () { },
render: function () {
this.$el.html(this.model.get("html"));
},
setModel: function (model) {
this.model = model;
var $this = this;
if (!this.model.get("loaded")) {
/*
this.model.fetch({ success: function () {
$this.model.set("loaded", true);
$this.render();
}
});*/
$this.model.set("html", "<h2>Full item " + this.model.get("id") + "</h2>");
$this.model.set("loaded", true);
$this.render();
} else {
$this.render();
}
}
});
var AccommodationListView = Backbone.View.extend({
tagName: "ul",
initialize: function () {
this.collection.on("reset", this.render, this);
},
render: function () {
this.addAll();
},
addOne: function (item) {
var itemView = new AccommodationItemView({ model: item });
this.$el.append(itemView.render().el);
},
addAll: function () {
this.collection.forEach(this.addOne, this);
}
});
var App = new (Backbone.Router.extend({
routes: {
"": "index",
"accommodation/:id": "show"
},
initialize: function () {
this.detailView = new DetailView({ model: new AccommodationItem({ id: 1 }) });
$("#detail").append(this.detailView.el);
this.accommodationList = new AccommodationList();
this.accommodationListView = new AccommodationListView({ collection: this.accommodationList });
$("#app").append(this.accommodationListView.el);
},
start: function () {
Backbone.history.start({ pushState: false });
},
index: function () {
this.fetchCollections();
},
show: function (id) {
var model = this.accommodationList.get(id);
this.detailView.setModel(model);
},
fetchCollections: function () {
var items = [{ id: 1, description: "item one" }, { id: 2, description: "item two" }, { id: 3, description: "item three" }];
this.accommodationList.reset(items);
}
}));
$(function () {
App.start();
});
EDIT: In a comment below I mentioned the Codeschool backbone.js tutorial. Just want to say that I have now finished BOTH parts of the course and it DOES cover exactly the AppView pattern described in the accepted answer. It's an excellent course and I thoroughly recommend it.
you have a few of the concepts mixed up.
There is too much to explain here, so I've (very roughly) put together a patch of your code that works as you intend. I would advise that you put it side-by-side with your own and see what I have done differently.
http://jsfiddle.net/wtxK8/2
A couple of things, you should not init Backbone.history from within a router. your 'init' should look something more like this
$(function () {
window.app = new App();
window.appView = new AppView({el:document});
Backbone.history.start({ pushState: true });
});
This is setting a 'wrapper' view than encompasses the entire page. Also, you have far too much logic in your router. Try to only use the router for routes. After my quick re factor, your router only contains this:
var App = Backbone.Router.extend({
routes: {
"": "index",
"accommodation/:id": "show"
},
show: function (id) {
var model = window.appView.accommodationList.get(id);
window.appView.detailView.setModel(model);
}
});
The AppView (that I have written for you now does all of that initialize work.
var AppView = Backbone.View.extend({
initialize : function(){
this.detailView = new DetailView({ model: new AccommodationItem({ id: 1 }) });
$("#detail").append(this.detailView.el);
this.accommodationList = new AccommodationList();
this.accommodationListView = new AccommodationListView({ collection: this.accommodationList });
$("#app").append(this.accommodationListView.el);
this.fetchCollections();
},
fetchCollections: function () {
var items = [
{ id: 1, description: "item one" },
{ id: 2, description: "item two" },
{ id: 3, description: "item three" }
];
this.accommodationList.reset(items);
}
});
Even after my re factor, it's still far from optimal, but I have provided it all to help you on your journey of learning :)
I would then recommend you follow some of the on-line tutorials step-by-step so that you can set up the structure of your app in a better way.
Good Luck, and be sure to check out http://jsfiddle.net/wtxK8/2 to see it working.
EDIT: I have not address your second question. there is enough to be worked on with question 1 to keep you busy. If I have more time later, I will help further.
I'm running into a problem maintaining my collection. First, I load attendees into a collection via fetch. This loads existing attendees from the database in to the collection. I also have a button which allows a user to add new attendees. When an attendee is manually entered it seems to wipe out the models loaded into the collection via fetch and starts fresh. All manually added attendees now populate the collection; however, i would like both the fetch loaded and manually added attendees to populate this list.
var InviteeView = Backbone.View.extend({
tagName: "tr",
initialize: function() {
this.collection = new InviteeJSONList();
_.bindAll(this, 'render','appendItem','remove','saveInvitee');
},
events: {
"click .removeInvitee":"remove",
"click .saveInvitee":"saveInvitee"
},
render: function() {
var source = $("#invitee-template").html();
var template = Handlebars.compile(source);
var context = inviteeListJSON.attributes['json'];
var html=template(context);
$(this.el).html(html);
return this;
},
appendItem: function() {
$("#attendees").append(this.render().el);
},
remove: function() {
$(this.el).remove();
},
saveInvitee: function() {
var value = $(this.el).find('select').val();
var model = this.collection.attributes['json']['invitees'];
model = model.filter(function(attributes) {return attributes.encrypted_id==value});
var attendee = new Attendee({
user_id: model[0]['id'],
meeting_id: '<?=$mid?>',
status: 'Uncomfirmed',
first_name: model[0]['first_name'],
last_name: model[0]['last_name'],
email: model[0]['email'],
user_uuid: model[0]['encrypted_id'],
unavailable_dates: model[0]['unavailable_dates']
});
attendeeView.addAttendeeItem(attendee.attributes)
this.remove();
}
});
var AttendeeList = Backbone.Collection.extend({
model: Attendee,
url: '<?=$baseURL?>api/index.php/attendees/<?=$mid?>×tamp=<?=$timestamp?>&userid=<?=$userid?>&apikey=<?=$apikey?>',
parse: function(response) {
if(response!="No History") {
$.each(response['attendees'], function(key, value) {
attendeeView.addAttendeeItem(value);
});
$('.loading_attendees').hide();
}
else {
$('.loading_attendees').html("No attendees exists for this meeting.");
}
}
});
var AttendeeView = Backbone.View.extend({
el: $('body'),
initialize: function() {
_.bindAll(this, 'render','fetchAttendees', 'appendItem', 'addAttendeeItem');
this.counter=0;
this.collection = new AttendeeList();
this.collection.bind('add', this.appendItem);
this.fetchAttendees();
},
events: {
"click #addInvitee":"appendInvitees",
},
appendInvitees: function() {
var inviteeView = new InviteeView();
inviteeView.appendItem();
},
render: function() {
},
fetchAttendees: function() {
this.collection.fetch({
success: function(model, response) {
},
error: function(model, response) {
$('#loading_attendees').html("An error has occurred.");
}
});
},
appendItem: function(item) {
var attendeeItemView = new AttendeeItemView({
model: item
});
$("#attendees").append(attendeeItemView.render().el);
attendeeItemView.updateAttendeeStatusSelect();
},
addAttendeeItem: function(data) {
this.counter++;
var attendee = new Attendee({
id: data['id'],
user_id: data['user_id'],
meeting_id: data['id'],
status: data['status'],
comments: data['comments'],
attended: data['datetime'],
first_name: data['first_name'],
last_name: data['last_name'],
email: data['email'],
counter: this.counter,
user_uuid: data['user_uuid'],
unavailable_dates: data['unavailable_dates']
});
this.collection.add(attendee);
},
});
After the collection (2 items loaded from REST API) is loaded via fetch():
console.log(this.collection.models) outputs:
[d]
[d,d]
Then when I manually add an attendee via a button the collection seems to reset:
console.log(this.collection.models) outputs:
[d]
Good that it's working, as there are many ways to go. I probably would have structured it a bit differently to leverage the Backbone methods that instantiate modes, but working code is the real goal, so these are just my thoughts:
Rather than actually instantiate the Models in the Collection parse() method, merely have parse return an array of data objects from which Backbone would instantiate the models, and trigger a
Rather than call fetch for the Collection inside AttendeeView, but outside the View class
Either have AttendeeView represent the view for a single attendee, or name it AttendeeListView and have it render the list
For instance:
AttendeeList = Backbone.Collection.extend({
...
parse: function(response) {
// create an array of objects from which the models can be parsed
var rawItems = [];
$.each(response['attendees'], function(key, value) {
rawItems.push({
id: data['id'],
user_id: data['user_id'],
meeting_id: data['id'],
status: data['status'],
comments: data['comments'],
attended: data['datetime'],
first_name: data['first_name'],
last_name: data['last_name'],
email: data['email'],
counter: this.counter,
user_uuid: data['user_uuid'],
unavailable_dates: data['unavailable_dates']
});
});
return rawItems;
},
...
}
and then either use the success/failure call backs:
AttendeeList.fetch( onListFetchSuccess , onListFetchFail );
or listen for the reset event that gets triggered:
AttendeeList.on('reset', createAttendeeListView );
(I didn't actually edit and run the code, this is just an outline)
I ended up resolving the issue by removing the url parameter and parse function out of the collection and into the view. Now everything works as expected.