Backbonejs when to initialize collections - javascript

I'm building small one page application with rails 3.1 mongodb and backbonejs.
I have two resources available through json api. I created two models and collections in backbone which look like this
https://gist.github.com/1522131
also I have two seprate routers
projects router - https://gist.github.com/1522134
notes router - https://gist.github.com/1522137
I generated them with backbonejs-rails gem from github so code inside is just template. I initialize my basic router inside index.haml file
#projects
:javascript
$(function() {
window.router = new JsonApi.Routers.ProjectsRouter({projects: #{#projects.to_json.html_safe}});
new JsonApi.Routers.NotesRouter();
Backbone.history.start();
});
I don't want fetch notes when application is starting, because there is big chance that user will never look inside notes. So there isn't good reason to fetch it on start. Inside NotesRouter in all action I rely on #notes variable but without .fetch() method this variable is empty. Also I should can reproduce notes view from url like
/1/notes/5
project_id = 1
note_id = 5
What is best practices in backbonejs to solve this kind of problem ?

Why don't you lazy load the notes when it's requested? Here's an example:
var State = Backbone.Model.extend({
defaults: {
ready: false,
error: null
}
});
var Note = Backbone.Model.extend({
initialize: function () {
this.state = new State();
}
});
var Notes = Backbone.Collection.extend({
model: Note,
initialize: function () {
this.state = new State();
}
});
var NoteCache = Backbone.Model.extend({
initialize: function () {
this._loading = false;
this._loaded = false;
this._list = new Notes();
},
_createDeferred: function (id) {
var note = new Note({ id: id });
this._list.add(note);
this._load();
return note;
},
getNote: function (id) {
return this._list.get(id) || this._createDeferred(id);
},
getNotes: function () {
if (!this._loaded)
this._load();
return this._list;
},
_load: function () {
var that = this;
if (!this._loading) {
this._list.state.set({ ready: false, error: null });
this._loading = true;
$.ajax({
url: '/api/notes',
dataType: 'json',
cache: false,
type: 'GET',
success: function (response, textStatus, jqXHR) {
_.each(response.notes, function (note) {
var n = that._list.get(note.id);
if (n) {
n.set(note);
} else {
that._list.add(note, { silent: true });
n = that._list.get(note.id);
}
n.state.set({ ready: true, error: null });
});
that._list.state.set({ ready: true, error: null });
that._list.trigger('reset', that._list);
that._loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
that._list.state.set({ error: 'Error retrieving notes.' });
that._list.each(function (note) {
note.state.set({ error: 'Error retrieving note.' });
});
},
complete: function (jqXHR, textStatus) {
that._loading = false;
}
});
}
}
});
In this example, I'm defining a NoteCache object that manages the lazy loading. I also add a "state" property to the Note model and Notes collection.
You'll probably want to initialize NoteCache somewhere (probably inside your route) and whenever you want a note or notes, just do this:
var note = noteCache.getNote(5);
var notes = noteCache.getNotes();
Now inside your view, you'll want to listen for state changes in case the note/notes is not loaded yet:
var NoteView = Backbone.View.extend({
initialize: function(){
this.note.state.bind('change', this.render, this);
},
render: function(){
if (this.note.state.get('error') {
// todo: show error message
return this;
}
if (!this.note.state.get('ready') {
// todo: show loader animation
return this;
}
// todo: render view
return this;
}
});
I haven't tested this, so there may be some bugs, but I hope you get the idea.

Related

Rendering the view returns undefined

I've got a collection view with two filter methods, and a render method which takes a parameter. The problem I'm stuck with is that when rendering the view for the first time it returns me an error. Here's my collection:
var ResumeCollection = Backbone.Collection.extend({
url: 'http://localhost:3000',
filterActive: function () {
var active = this.where({interviewed: false});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return new ResumeCollection(interviewed);
}
});
And my view:
var ResumeList = Backbone.View.extend({
events { // hash array of filter events },
initialize: function () {
this.collection.fetch();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
showActive: function (ev) {
var filtered = this.collection.filterActive();
this.render(filtered);
},
showInterviewed: function (ev) {
var filtered = this.collection.filterInterviewed();
this.render(filtered);
},
showAll: function (ev) {
this.render(this.collection);
}
});
This view gets rendered for the first time in my router by passing a collection:
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function () {
this.layout = new LayoutView();
}
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
});
And this is the layout view within which all the other views are rendered:
var LayoutView = Backbone.View.extend({
el: $('#outlet'),
render: function (view) {
if (this.child && this.child !== view) {
this.child.undelegateEvents();
}
this.child = view;
this.child.setElement(this.$el).render();
return this;
}
});
When I just refresh my page, I get filtered.toArray is not a function error and nothing is rendered respectively. After inspecting everything in the debugger, I found out that when the view gets rendered for the first time, the filtered attribute receives an empty collection, assigns it to data variable, which becomes an empty array and goes to the body of render function, becoming undefined after that. The mysteries go here: whenever I click items, that are bound to my show* events, they act exactly as expected and render either models where interviewed === false, or true or the whole collection. This looks kinda magic to me and I haven't got the faintest idea what can I do with that.
ADDED: GitHub repo with this project
Your home function on the AppRouter has a typo. You have an extra semi-colon.
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection();
}));
}
Should be
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
I needed to remove it to get the JSFiddle working: https://jsfiddle.net/4gyne5ev/1/
I'd recommend adding some kind of linting tool into your IDE or Build process (http://eslint.org/)
You need to add home url content to your db.json file like this
"" : [
{
'somthing': 'somthing'
}
]
After a piece of advice from my mentor I realized that the core of the problem was in asynchronous origin of fetch method -- as I passed this.collection.fetch in my initialize function, it executed after my render method, not before it, so my render method had just nothing to render when the view was called for the first time. So, this fix worked:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection();
// removed .fetch() method from here
},
render: function (filtered) {
var self = this;
var data;
// and added it here:
this.collection.fetch({
success: function (collection) {
if (!filtered) {
data = collection.toArray();
} else {
data = filtered.toArray();
}
self.$el.html(self.template(collection.toJSON()));
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
})
}
});
}
});
And this worked perfectly and exactly as I needed.

Backbone - model.destroy() function not defined for model

For some reason, I am getting a TypeError in my JavaScript regarding a supposed Backbone model object for which I am trying to call "model.destroy()":
Here's my Backbone code:
var Team = Backbone.Model.extend({
idAttribute: "_id",
urlRoot: '/api/teams'
});
var TeamCollection = Backbone.Collection.extend({
model: Team
});
var teamCollection = new TeamCollection([]);
teamCollection.url = '/api/teams';
teamCollection.fetch(
{
success: function () {
console.log('teamCollection length:', teamCollection.length);
}
}
);
var UserHomeMainTableView = Backbone.View.extend({
tagName: "div",
collection: teamCollection,
events: {},
initialize: function () {
this.collection.on("reset", this.render, this);
},
render: function () {
var teams = {
teams:teamCollection.toJSON()
};
var template = Handlebars.compile( $("#user-home-main-table-template").html());
this.$el.html(template(teams));
return this;
},
addTeam: function (teamData) {
console.log('adding team:', team_id);
},
deleteTeam: function (team_id) {
console.log('deleting team:', team_id);
var team = teamCollection.where({_id: team_id}); //team IS defined here but I can't confirm the type even when logging "typeof"
console.log('team to delete', typeof team[0]);
console.log('another team to delete?',typeof team[1]);
team.destroy({ //THIS FUNCTION CALL IS THROWING A TYPEERROR
contentType : 'application/json',
success: function(model, response, options) {
this.collection.reset();
},
error: function(model, response, options) {
this.collection.reset();
}
});
}
});
So I am fetching the data from the node.js server, and the server is returning JSON. The JSON has cid's and all that jazz, so those objects were once Backbone models at some point.
I just don't know why the type of team would not be a Backbone model.
Any ideas?
.where returns an array. You need to use .findWhere instead.
Or call destroy for every model in the resulting array.
.where({...}).forEach(function(model){
model.destroy();
});

Update url using router

I have a router on my application now it respond as expected when I type in the url. For exapmple if I type in www.example.com#search/groupa, I get the appropriate result back. I have attempted in the search feature to call navigate to set the url so that a user could cut and paste that and send it to another user. The issue is it doesnt work I get the following error when attempting to do so: "Uncaught TypeError: Object function (){return i.apply(this,arguments)} has no method 'navigate'"
IEG = new Backbone.Marionette.Application();
IEG.addRegions({
searchBox: '#searchBox',
resultBox: '#resultBox',
modalBox: '#modalBox',
recipientBox: '#recipientBox',
confirmBox: '#confirmToggleActive'
});
IEG.vent = _.extend({}, Backbone.Events);
IEG.vent.on("default", function () {
var SBV = new SearchBoxView();
IEG.searchBox.show(SBV);
IEG.searchColl = new GroupEntries();
IEG.searchColl.fetch({
data: {
cmd: 0, //search groups
searchStr: null //if null show all groups
},
success: function (data) {
searchResults = new SearchResultsView({ collection: IEG.searchColl });
IEG.resultBox.show(searchResults);
}
});
});
IEG.vent.on("searchGroups", function (searchStr) {
IEG.Router.navigate("search" + searchStr); // CALLING NAVIGATE HERE
IEG.searchColl.fetch({
data: {
cmd: 0, //search groups
searchStr: searchStr
},
success: function (data) {
searchResults = new SearchResultsView({ collection: IEG.searchColl });
IEG.resultBox.show(searchResults);
}
});
});
IEG.Router = Backbone.Router.extend({
routes: {
'': 'index',
'search/:str': 'search',
'edit/:grp': 'edit'
},
index: function () {
IEG.vent.trigger("default");
},
search: function (str)
{
IEG.vent.trigger("searchGroups",str);
}
});
$(document).ready(function () {
IEG.start();
new IEG.Router;
Backbone.history.start();
});
You need to call navigate on the instance of the Router class and not on its definition (as you're currently doing). Try updating the code in your document ready handler like this:
$(document).ready(function () {
IEG.start();
IEG.router = new IEG.Router(); // Store an instance of the router on the Application
Backbone.history.start();
});
And your searchGroups handler like this:
IEG.vent.on("searchGroups", function (searchStr) {
IEG.router.navigate("search" + searchStr); // call navigate on the instance
// Fetch code .....
});

GUI component for display async request states

Which interface or component do you suggest to display the state of parallel async calls? (The language is not so important for me, just the pattern, I can rewrite the same class / interface in javascript...)
I load model data from REST service, and I want to display pending label before the real content, and error messages if something went wrong... I think this is a common problem, and there must be an already written component, or best practices, or a pattern for this. Do you know something like that?
Here is a spaghetti code - Backbone.syncParallel is not an existing function yet - which has 2 main states: updateForm, updated. Before every main state the page displays the "Please wait!" label, and by error the page displays an error message. I think this kind of code is highly reusable, so I think I can create a container which automatically displays the current state, but I cannot decide what kind of interface this component should have...
var content = new Backbone.View({
appendTo: "body"
});
content.render();
var role = new Role({id: id});
var userSet = new UserSet();
Backbone.syncParallel({
models: [role, userSet],
run: function (){
role.fetch();
userSet.fetch();
},
listeners: {
request: function (){
content.$el.html("Please wait!");
},
error: function (){
content.$el.html("Sorry, we could not reach the data on the server!");
},
sync: function (){
var form = new RoleUpdateForm({
model: role,
userSet: userSet
});
form.on("submit", function (){
content.$el.html("Please wait!");
role.save({
error: function (){
content.$el.html("Sorry, we could not save your modifications, please try again!");
content.$el.append(new Backbone.UI.Button({
content: "Back to the form.",
onClick: function (){
content.$el.html(form.$el);
}
}));
},
success: function (){
content.$el.html("You data is saved successfully! Please wait until we redirect you to the page of the saved role!");
setTimeout(function (){
controller.read(role.id);
}, 2000);
}
});
}, this);
form.render();
content.$el.html(form.$el);
}
}
});
I created a custom View to solve this problem. (It is in beta version now.)
Usage: (Form is a theoretical form generator)
var content = new SyncLabelDecorator({
appendTo: "body",
});
content.load(function (){
this.$el.append("normal html without asnyc calls");
});
var User = Backbone.Model.extend({
urlRoot: "/users"
});
var UserSet = Backbone.Collection.extend({
url: "/users",
model: User
});
var Role = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'members',
relatedModel: User
}]
});
var administrator = new Role({id :1});
var users = new UserSet();
content.load({
fetch: [role, users],
sync: function (){
var form = new Form({
title: "Update role",
model: role,
fields: {
id: {
type: "HiddenInput"
},
name: {
type: "TextInput"
},
members: {
type: "TwoListSelection",
alternatives: users
}
},
submit: function (){
content.load({
tasks: {
save: role
},
sync: function (){
this.$el.html("Role is successfully saved.");
}
});
}
});
this.$el.append(form.render().$el);
}
});
Code:
var SyncLabelDecorator = Backbone.View.extend({
options: {
pendingMessage: "Sending request. Please wait ...",
errorMessage: "An unexpected error occured, we could not process your request!",
load: null
},
supported: ["fetch", "save", "destroy"],
render: function () {
if (this.options.load)
this.load();
},
load: function (load) {
if (load)
this.options.load = load;
this._reset();
if (_.isFunction(this.options.load)) {
this.$el.html("");
this.options.load.call(this);
return;
}
_(this.options.load.tasks).each(function (models, method) {
if (_.isArray(models))
_(models).each(function (model) {
this._addTask(model, method);
}, this);
else
this._addTask(models, method);
}, this);
this._onRun();
_(this.tasks).each(function (task) {
var model = task.model;
var method = task.method;
var options = {
beforeSend: function (xhr, options) {
this._onRequest(task, xhr);
}.bind(this),
error: function (xhr, statusText, error) {
this._onError(task, xhr);
}.bind(this),
success: function (data, statusText, xhr) {
this._onSync(task, xhr);
}.bind(this)
};
if (model instanceof Backbone.Model) {
if (method == "save")
model[method](null, options);
else
model[method](options);
}
else {
if (method in model)
model[method](options);
else
model.sync(method == "fetch" ? "read" : (method == "save" ? "update" : "delete"), model, options);
}
}, this);
},
_addTask: function (model, method) {
if (!_(this.supported).contains(method))
throw new Error("Method " + method + " is not supported!");
this.tasks.push({
method: method,
model: model
});
},
_onRun: function () {
this.$el.html(this.options.pendingMessage);
if (this.options.load.request)
this.options.load.request.call(this);
},
_onRequest: function (task, xhr) {
task.abort = function () {
xhr.abort();
};
},
_onError: function (task, xhr) {
this._abort();
this.$el.html(this.options.errorMessage);
if (this.options.load.error)
this.options.load.error.call(this);
},
_onSync: function (task, xhr) {
++this.complete;
if (this.complete == this.tasks.length)
this._onEnd();
},
_onEnd: function () {
this.$el.html("");
if (this.options.load.sync)
this.options.load.sync.call(this);
},
_reset: function () {
this._abort();
this.tasks = [];
this.complete = 0;
},
_abort: function () {
_(this.tasks).each(function (task) {
if (task.abort)
task.abort();
});
}
});

Backbone - Validation not working on create, only update/edit?

So, I am able to validate just fine when I am editing an existing item. However, if I want to create, validation for some reason is not getting kicked off. Instead, I am seeing the errors below:
//this is if the field I want to validate is empty
Uncaught TypeError: Object #<Object> has no method 'get'
//this is if everything in the form is filled out
Uncaught TypeError: Cannot call method 'trigger' of undefined
Here is(what I think is) the relative portion of my js. Sorry if its an overload, I wanted to add as much as I can to be as specific as possible:
Comic = Backbone.Model.extend({
initialize: function () {
this.bind("error", this.notifyCollectionError);
this.bind("change", this.notifyCollectionChange);
},
idAttribute: "ComicID",
url: function () {
return this.isNew() ? "/comics/create" : "/comics/edit/" + this.get("ComicID");
},
validate: function (atts) {
if ("Name" in atts & !atts.Name) {
return "Name is required";
}
if ("Publisher" in atts & !atts.Publisher) {
return "Publisher is required";
}
},
notifyCollectionError: function (model, error) {
this.collection.trigger("itemError", error);
},
notifyCollectionChange: function () {
this.collection.trigger("itemChanged", this);
}
});
Comics = Backbone.Collection.extend({
model: Comic,
url: "/comics/comics"
});
comics = new Comics();
FormView = Backbone.View.extend({
initialize: function () {
_.bindAll(this, "render");
this.template = $("#comicsFormTemplate");
},
events: {
"change input": "updateModel",
"submit #comicsForm": "save"
},
save: function () {
this.model.save(
this.model.attributes,
{
success: function (model, response) {
model.collection.trigger("itemSaved", model);
},
error: function (model, response) {
model.trigger("itemError", "There was a problem saving " + model.get("Name"));
}
}
);
return false;
},
updateModel: function (evt) {
var field = $(evt.currentTarget);
var data = {};
var key = field.attr('ID');
var val = field.val();
data[key] = val;
if (!this.model.set(data)) {
//reset the form field
field.val(this.model.get(key));
}
},
render: function () {
var html = this.template.tmpl(this.model.toJSON());
$(this.el).html(html);
$(".datepicker").datepicker();
return this;
}
});
NotifierView = Backbone.View.extend({
initialize: function () {
this.template = $("#notifierTemplate");
this.className = "success";
this.message = "Success";
_.bindAll(this, "render", "notifySave", "notifyError");
comics.bind("itemSaved", this.notifySave);
comics.bind("itemError", this.notifyError);
},
events: {
"click": "goAway"
},
goAway: function () {
$(this.el).delay(0).fadeOut();
},
notifySave: function (model) {
this.message = model.get("Name") + " saved";
this.render();
},
notifyError: function (message) {
this.message = message;
this.className = "error";
this.render();
},
render: function () {
var html = this.template.tmpl({ message: this.message, className: this.className });
$(this.el).html(html);
return this;
}
});
var ComicsAdmin = Backbone.Router.extend({
initialize: function () {
listView = new ListView({ collection: comics, el: "#comic-list" });
formView = new FormView({ el: "#comic-form" });
notifierView = new NotifierView({el: "#notifications" });
},
routes: {
"": "index",
"edit/:id": "edit",
"create": "create"
},
index: function () {
listView.render();
},
edit: function (id) {
listView.render();
$(notifierView.el).empty();
$(formView.el).empty();
var model = comics.get(id);
formView.model = model;
formView.render();
},
create: function () {
var model = new Comic();
listView.render();
$(notifierView.el).empty();
$(formView.el).empty();
formView.model = model;
formView.render();
}
});
jQuery(function () {
comics.fetch({
success: function () {
window.app = new ComicsAdmin();
Backbone.history.start();
},
error: function () {
}
});
})
So, shouldnt my create be getting validated too? Why isnt it?
When creating a new instance of a model, the validate method isn't called. According to the backbone documentation the validation is only called before set or save.
I am also struggling with this problem and found solutions in related questions:
You could make a new model and then set its attributes (see question 9709968)
A more elegant way is calling the validate method when initializing the model (see question 7923074)
I'm not completely satisfied with these solutions because creating a new instance of the model like described in the backbone documentation shouldn't happen when an error is triggered. Unfortunately, in both solutions you're still stuck with a new instance of the model.
edit: Being stuck with a new instance of the model is actually quite nice. This way you can give the user feedback about why it didn't pass the validator and give the opportunity to correct his/her input.
OK. So, I'm having some mild success here.
First, I wrote my own validation framework, Backbone.Validator since I didn't like any of the ones out there that I found.
Second, I am able to get the validation framework to set off the validation routine by setting silent: false with in the object provided during the new Model creation.
Along with using the use_defaults parameter from my validation framework I am able to override bad data during setup in initial testing. I'm still working on doing some more tests on this, but it seems to be going OK from from the Chrome browser console.

Categories

Resources