best way to realize backbone list application - javascript

I use backbone.boilerplate for creating a simple application.
I want to create module that can show collections of sites. Each sites has id and title attributes (example [{ id: 1, title: "github.com" }, { id: 2, title: "facebook.com" }].
My router:
routes: {
"": "index",
"sites": "sites"
},
sites: function () {
require(['modules/sites'], function (Sites) {
var layout = new Sites.Views.Layout();
app.layout.setView('#content', layout);
});
}
So, my sites module has layout, which do this:
Sites.Views.Layout = Backbone.Layout.extend({
template: "sites/layout",
className: 'container-fluid',
initialize: function () {
_.bindAll(this);
this.collection = new Sites.Collections.Sites();
this.collection.fetch({
success: this.render
});
},
beforeRender: function () {
var siteList = new Sites.Views.SiteList({
collection: this.collection
});
this.setView('.content', siteList);
},
});
Sites.Views.SiteList = Backbone.View.extend({
template: 'sites/list',
beforeRender: function () {
this.collection.each(function (model) {
var view = new Sites.Views.SiteItem({
model: model
});
this.insertView('tbody', view);
}, this);
}
});
Sites.Views.SiteItem = Backbone.View.extend({
template: 'sites/item',
tagName: 'tr',
serialize: function () {
return {
title: this.model.get('title')
};
}
});
ok. and now my question: help me please to choose best way to render one site view when user click on element of collection. I want that it is works like gmail: one screen for all letters and all screen for one letter when it choosed. Maybe you have link with example of similar application. I am waiting for your advices.

Looking at your pastebin code it seems like you have a basic understanding of Backbone, which is certainly all you need to get started.
That being said, you might find this article/tutorial helpful. It walks through the process of building inter-connected views (in the tutorial they are related <select> elements) which use AJAX to update their values:
http://blog.shinetech.com/2011/07/25/cascading-select-boxes-with-backbone-js/

Related

Marionette.js CompositeView - How to render model and collection without nesting

I'd like to ask for some help because I've been trying to solve this for some time now. I've read Derick Bailey's blog post on tree structures and CompositeViews. I also read David Sulc's but I have what I think is a slightly different use case than the one described there. Note: My project uses Marionette.js 1.0.3.
I am trying to build something that will work like an inbox with emails displayed in a table. An email may be a thread, meaning that it will have other emails that are linked to it. The inbox view is being rendered in a <table> where each <tr> is an email. My JSON looks like:
[
{id: 1, subject: 'One', threads: []},
{id: 2, subject: 'Two', threads: [
{id: 3, subject: 'Three', threads: []},
{id: 4, subject: 'Four', threads: []}
]},
{id: 5, subject: 'Five', threads: []}
]
My views are configured like this:
InboxView = Marionette.CompositeView.extend({
// edited for brevity
initialize: function(options) {
this.itemView = EmailView;
}
// edited for brevity
});
EmailView = Marionette.CompositeView.extend({
// edited for brevity
tagName: 'tr',
initialize: function(options) {
this.collection = this.model.get('threads');
},
onRender: function() {
if (this.model.isThread()) this.$el.addClass('thread');
}
// edited for brevity
});
The issue I'm having is that if I let CompositeView work its magic for me by rendering the model once and then the collection of threads once, I end up with two table rows <tr> (one for each thread) inside the <tr> for the original email (parent).
There exists functionality in the InboxView and EmailView that I'm trying to reuse. What I'm trying to end up with is a table that has all rows shown at the same level.
If you're reading this and want to help me, thank you in advance!
First of all you should attach views to the DOM. Errors occur, because child views rendered before they are attached to the DOM. You can override some methods to solve the problem. That will do the trick:
EmailView = Marionette.CompositeView.extend({
className: function () {
return this.model.isThread() ? 'thread' : null;
},
initialize: function (options) {
this.collection = new Backbone.Collection(this.model.get('threads'));
},
renderItemView: function(view, index) {
this.$el.parent().append(view.el);
view.render();
}
});
InboxView = Marionette.CompositeView.extend({
itemView: EmailView,
ui: {
$tbody: 'tbody'
},
renderItemView: function (view, index) {
this.ui.$tbody.append(view.el);
view.render();
}
});
JSFiddle: http://jsfiddle.net/d1krtxtr/

Giving a single reference to multiple Backbone.Models

I have a Backbone.Model which looks something like:
var FooModel = Backbone.Model.extend({
defaults: {
details: '',
operatingSystem: ''
};
});
There are many instances of FooModel which are stored in a collection:
var FooCollection = Backbone.Collection.extend({
model: FooModel
});
FooModel's OperatingSystem is a property which only needs to be calculated once and is derived asynchronously. For example:
chrome.runtime.getPlatformInfo(function(platformInfo){
console.log("Operating System: ", platformInfo.os);
});
If I perform this logic at the FooModel level then I will need to perform the logic every time I instantiate a FooModel. So, I think that this operation should be performed at a higher level. However, it is bad practice to give properties to a Backbone.Collection.
As such, this leaves me thinking that I need a parent model:
var FooParentModel = Backbone.Model.extend({
defaults: {
platformInfo: '',
fooCollection: new FooCollection()
},
initialize: function() {
chrome.runtime.getPlatformInfo(function(platformInfo){
this.set('platformInfo', platformInfo);
}.bind(this));
},
// TODO: This will work incorrectly if ran before getPlatformInfo's callback
createFoo: function(){
this.get('fooCollection').create({
details: 'hello, world',
operatingSystem: this.get('platformDetails').os
});
}
});
This works and is semantically correct, but feels over-engineered. The extra layer of abstraction feels unwarranted.
Is this the appropriate way to go about giving a property to a model?
Although Backbone Collections may not have attributes, they may have properties (as well as any object) which you can use to store shared data.
var FooCollection = Backbone.Collection.extend({
model: FooModel
initialize: function() {
this.platformInfo = null; // shared data
chrome.runtime.getPlatformInfo(function(platformInfo){
this.platformInfo = platformInfo;
}.bind(this));
},
// wrapper to create a new model within the collection
createFoo: function(details) {
this.create({
details: details,
operatingSystem: this.platformInfo? this.platformInfo.os : ''
});
}});
});

general backbone/marionette program structure

I need some general guidelines on how to structure a backbone/marionette application. Im very new to these frameworks and also to js in general.
Basically I have two pages and each page has a list. I have set up an application and a router for the application:
var app = new Backbone.Marionette.Application();
app.module('Router', function(module, App, Backbone, Marionette, $, _){
module.AppLayoutView = Marionette.Layout.extend({
tagName: 'div',
id: 'AppLayoutView',
template: 'layout.html',
regions: {
'contentRegion' : '.main'
},
initialize: function() {
this.initRouter();
},
...
});
module.addInitializer(function() {
var layout = new module.AppLayoutView();
app.appRegion.show(layout);
});
});
In the initRouter I have two functions, one for each page that gets called by router depending on the url.
The function for the content management page looks like this:
onCMNavigated: function(id) {
var cMModule = App.module("com");
var cM = new cMModule.ContentManagement({id: id, region: this.contentRegion});
contentManagement.show();
this.$el.find('.nav-item.active').removeClass('active');
this.cM.addClass('active');
}
So if this is called, I create a new instance of ContentManagement model. In this model, when show() is called, I fetch the data from a rest api, and I parse out an array of banners that need to be shown in a list view. Is that ok? The model looks like the following:
cm.ContentManagement = Backbone.Model.extend({
initialize: function (options) {
this.id = options.id;
this.region = options.region;
},
show: function() {
var dSPage = new DSPage({id: this.id});
dSPage.bind('change', function (model, response) {
var view = new cm.ContentManagementLayoutView();
this.region.show(view);
}, this);
dSPage.fetch({
success: function(model, response) {
// Parse list of banners and for each create a banner model
}
}
});
cm.ContentManagementLayoutView = Marionette.Layout.extend({
tagName: 'div',
id: 'CMLayoutView',
className: 'contentLayout',
template: 'contentmanagement.html',
regions: {
'contentRegion' : '#banner-list'
}
});
Now my biggest doubt is how do I go on from here to show the banner list? I have created a collectionview and item view for the banner list, but is this program structure correct?
do You really need marionnete to manage your application ? especially You are beginner as me too :)
try pure backbone first. You can still use marionette as a library.
backbone MVC architecture is described perfectly on many sites.

Dynamically changing url in Backbone

I was trying to dynamically change the url inside the router but couldn't manage to do it, it keeps returning to the base Collection URL. Here i posted the code with the 3 different collections which apart from pointing to three different urls they do exactly the same.
I have only one model and three collections that depend on that model and they even render the same view. How can i dynamically change the url so i can create only one Collection and one Model? Is it best pracitce for a case like this?
// MODELS & COLLECTIONS
window.Post = Backbone.Model.extend({
urlRoot: function() {
return 'http://localhost:5000/json/guides/:id'
}
})
App.Collections.RecentPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/recent',
})
App.Collections.PopularPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/popular',
})
App.Collections.FeaturedPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/featured',
})
// CONTROLLER
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
recent: function(){
//.... same as featured ?
},
popular: function(){
//.... same as featured ?
},
featured: function(){
$("#browser").empty();
var collection = new App.Collections.Posts();
collection.fetch({
success: function(col,posts){
new App.Views.GuideView({collection: posts});
},
error: function(error){
console.log(error)
}
})
}
});
There are numerous different ways of doing this. Here's what's probably going to be 'best practice'.
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
initialize: function () {
this.collection = new App.Collections.Posts();
},
_showPage: function (config) {
$("#browser").empty();
this.collection.fetch({
url: config.url,
success: function(col,posts){
new App.Views.GuideView({collection: posts});
},
error: function(error){
console.log(error)
}
});
},
recent: function(){
this._showPage({url:'http://localhost:5000/json/posts/recent'});
},
popular: function(){
this._showPage({url:'http://localhost:5000/json/posts/popular'});
},
featured: function(){
this._showPage({url:'http://localhost:5000/json/posts/featured'});
}
});
Since I really don't know how complicated your page is going to get, this is probably the best I can do without more information. But, the idea is that "this.collection" is set on the routers initialization.. so you can keep reusing it. The _showPage method does whatever basic tasks you need done to show the page, and the methods called by the routes use it to do whatever basic stuff needs done before going into detail. The url passed into the config would simply tell the collection where to get its information from - I'm assuming that all of your data has the same format and 'is the same thing'.. just different filters.
You can probably do a similar thing with App.Views.GuideView:
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
initialize: function () {
this.collection = new App.Collections.Posts();
this.view = new App.Views.GuideView({collection: this.collection});
},
_showPage: function (config) {
$("#browser").empty();
this.collection.fetch({
url: config.url,
success: _.bind(function(col,posts){
this.view.render();
}, this),
error: function(error){
console.log(error)
}
});
},
recent: function(){
this._showPage({url:'http://localhost:5000/json/posts/recent'});
},
popular: function(){
this._showPage({url:'http://localhost:5000/json/posts/popular'});
},
featured: function(){
this._showPage({url:'http://localhost:5000/json/posts/featured'});
}
});
The 'render' would just rebuild the view, and since you've already got the collection referenced in the view as "this.options.collection" (or you could add an 'initialize' to the view and set this.collection to be this.options.collection). When the collection gets updated, all of that information is by reference in the view.. so no need to reset it.
I think the best pratice would be to have 3 collections, each with it's on URL and properties.
This makes the code easier to maintain as you can assign different events and listeners to them in a separate file instead of having a "God Collection" that have all the logic inside it.
Of course you can still be DRY and keep a helper object or a parent collection with code that is commmon to all those collections.

Backbone Collection refresh all items

I'm having problems refreshing collection or more precisely collection view after updating all models on the server. Here's my scenario:
I have a collection of questions fetched from the server. Each question has a position attribute so I can manipulate the order in the list and save it back to the server with appropriate order.
I have a view for each single list item and a view with a more global scope that generates each list items and updates the collection. Basically I was using an example from O'Reilly book "Javascript Web Applications" which resembles a lot to the famous Todo annotated tutorial found here: http://documentcloud.github.com/backbone/docs/todos.html
So the structure is almost identical apart from a few custom models. Everythings works fine.
However, I'm having problems updating the collection with I reorder items in the
I've a method in my global view which fires evert time I drag list items in the list. Btw it works well and updates the order of the items on the server, but I also want to be able to update the digit in from of each item in the list.
window.QuestionView = Backbone.View.extend({
el: $("#content"),
events : {
'sortupdate ol#questions': 'sortStuff'
},
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'render', 'addNewItem', 'addItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('all', this.render);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
},
render: function() {},
sortStuff: function() {
$this = this;
$.ajax({
url: "/hq/reorder/",
type: "POST",
data: $("#questions").sortable("serialize")+"&id="+$qid,
dataType: 'json',
success: function(data) {
}
});
},
addItem: function() {
this.collection.add({title: 'New title goes here'});
return false;
},
addNewItem: function(question) {
var view = new ItemView({model: question, parent: this});
var element = view.render().el;
this.$("#questions").prepend(element);
$that = this;
$(view.render().el).addClass('new');
},
addOne: function(question) {
var view = new ItemView({model: question, parent: this});
this.$("#questions").prepend(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne);
return false;
}
});
So my question is.. what do I do on success: to be able to refresh each little model separately so it updates the digit to the proper order? Maybe some sort of _.each on Collection? Or maybe some sort of global view refresh on the whole collection?
Also my
success: function(data)
returns the new order as a list (or JSON object) from the server. maybe I can reuse this order to set each model with a new value without making unnecessary fetch() call on the server each time the order is changed?
EDIT:
I finally managed to get it to work with a reset, clearing the view and re-fetching a new collection. Perhaps it isn't the best way to do it since there's additional call to the server with a fetch()..
sortStuff: function() {
$this = this;
$.ajax({
url: "/hq/reorder/",
type: "POST",
data: $("#questions").sortable("serialize")+"&id="+$qid,
dataType: 'json',
success: function(data) {
$this.rerender();
}
});
},
rerender: function() {
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
$("#questions").html("");
this.collection.reset();
this.addAll();
},
I think your approach should be in two separate steps:
1) On one hand you update the data on the server
2) On the other hand you update the collection client-side
So, you are Ok on step 1, you said it works.
For step 2, you can take advantage of the event driven programming.
The logic is this one:
YOU JUST ADD ONE ELEMENT TO THE COLLECTION (collection.add(model) fires an 'add' event).
In the collection, you listen for the 'add' event. When you catch it, you sort your collection again (collection.sort fires a 'reset' event)
In your view for the list (questionView in your case) you listen for the collection reset event, and once it is fired you re-render your view
Example code:
1) QuestionView: addItem removed and addNewItem simplified (it must no render)
window.QuestionView = Backbone.View.extend({
el: $("#content"),
events : {
'sortupdate ol#questions': 'sortStuff'
},
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'addNewItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
},
render: function() {},
sortStuff: function() {
$this = this;
$.ajax({
url: "/hq/reorder/",
type: "POST",
data: $("#questions").sortable("serialize")+"&id="+$qid,
dataType: 'json',
success: function(data) {
}
});
},
//METHOD addItem REMOVED!!!
addNewItem: function(question) {
this.collection.add({title: 'New title goes here'}); //***IT FIRES AN ADD EVENT
},
addOne: function(question) {
var view = new ItemView({model: question, parent: this});
this.$("#questions").prepend(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne);
return false;
}
});
2) the collection catch the add event and sorts (trigger 'reset' event)
you can handle it always in the QuestionView, your initialize function becomes.
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'addNewItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
//ADD THIS*****
this.collection.on('add', function(){
this.collection.sort();
});
},
3) the third step is already done, you just re-render the view
The best would be that you sort elements in your collection defining a new 'comparator' function, which uses the 'position' attribute of your list
something like (in QuestionView)
this.collection.comparator: function(){
return this.collection.get("position");
}
so that items get ordered by position CLIENT SIDE
**EDIT**
Initialize function modified. Fetch is used instead of 'sort', which is unuseful as long as the 'position' attribute of each element in the collection is not updated.
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'addNewItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
//ADD THIS*****
this.collection.on('add', function(){
this.collection.fetch();
});
You should do Questions.reset(data);, however you need to tell ajax that the response is json:
sortStuff: function() {
$.ajax({
url: '/order',
data: $("ol#questions").sortable('serialize'),
dataType: 'json',
success: function(data) {
// now data is an object
Questions.reset(data);
});
});
}
I hope you have learned that backbone is event driven, and that you have an event for collection reset bound to the render method, so there's no need to explicitly call render here.
EDIT:
Ok, now with the code I see what you're trying to accomplish, and your logic is flawed. You shouldn't wait for the ajax call to return the new order. It's better if you update the order on the client side, and then save the model.
Here's a jsfiddle sample: http://jsfiddle.net/b75px/
Combine that with backbone, and you should have:
Note that I'm just guessing how you have organized the questions. If you have any other problems, update your question with more details.
events: {
'sortstart ol#questions': 'startSortingQuestions',
'sortupdate ol#questions': 'updateQuestions'
},
startSortingQuestions: function(event, ui) {
this.beforeIndex = ui.item.index();
},
updateQuestions: function(event, ui) {
var before = this.beforeIndex,
after = ui.item.index();
// now that you have the index change, all you have to do is set the new index
// and save them to the database
this.collection.at(before).set({ index: after; }).save();
this.collection.at(after).set({ index: before; }).save();
// passing silent: true because sort invokes the reset event,
// and we don't need to redraw anything
this.collection.sort({ silent: true });
/* or maybe an even better approach:
var beforeModel = this.collection.at(before);
var toSwapModel = this.collection.at(after);
this.collection.remove(beforeModel);
this.collection.remove(toSwapModel);
this.collection.add(beforeModel, { at: after, silent: true });
this.collection.add(toSwapModel, { at: before, silent: true });
note that I have no idea on how your database is structured, so my guess is
every question has it's index so you know it's order
therefore, you should still update the model's index field or whatever it is you're doing
*/
}

Categories

Resources