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

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/

Related

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.

Make ember to resolve hasMany relationship when loading

I'm currently facing a big problems for days. I'm using ember simple-auth plugin which provide me a session object accessible through the code or the templates. That session object store the account information such as username, id and rights.
My models are like this :
App.Right = DS.Model.extend({
label: DS.attr('string', { defaultValue: undefined })
});
App.Right.FIXTURES = [
{
id: 1,
label: 'Admin'
}, {
id: 2,
label: 'Manager'
}, {
id: 3,
label: 'User'
}
];
App.User = DS.Model.extend({
username: DS.attr('string'),
rights: DS.hasMany('right', {async: true})
});
App.User.FIXTURES = [
{
id: 1,
username: "Someone",
rights: [1]
}
];
Then I have (as specified on the simple-auth documentation) this setup :
App.initializer({
name: 'authentication',
initialize: function(container, application) {
Ember.SimpleAuth.Session.reopen({
account: function() {
var userId = this.get('userId');
if (!Ember.isEmpty(userId)) {
return container.lookup('store:main').find('user', userId);
}
}.property('userId')
});
...
}
});
Inside one of my view I'm doing this:
this.get('context.session.account.rights').toArray()
but it gives me an empty array. That piece of code is executed inside an Ember.computed property.
The question is how can I resolve the childrens of account before rendering the view ?
Since async: true this.get('context.session.account.rights') will return a promise object so you will have to use this.get('context.session.account.rights').then(... see: http://emberjs.com/api/classes/Ember.RSVP.Promise.html#method_then
Okay so I finally got it to work. It doesn't solve the original question because the original question was completely stupid. It's just IMPOSSIBLE to resolve relationships synchronously when you use the async: true. Trying to resolve it in advance is NOT the solution because you will still not know when it has actually resolved.
So here is the solution:
$.each(this.get('cellContent.buttonList'), function(i, button) {
button.set('hasAccess', false);
this.get('context.session.account').then(function(res) {
res.get('rights').then(function(result) {
button.set('hasAccess', Utils.hasAccess(result.toArray(), button.rights));
});
});
});
Using the following cellContent.buttonList definition:
buttonList: [
Ember.Object.create({
route: 'order',
label: 'Consult',
rights: 'all'
}), Ember.Object.create({
route: 'order.edit',
label: 'Edit',
rights: [1, 2]
})
]
Explanation
We have to use Ember.Object in order to have access to the set method. Using an Ember object is very handy. It allows us to change the value of properties after the render process making the view to update according to the new value you just set.
Because it updates the view, you don't have to care anymore whether your model has resolved or not.
I hope this will help people as much as it helps me.

Home/away teams with a list of players inside a backbone.js app?

A. What level I am at: intro I just finished a backbone.js course from "Jeffery Way". I am getting the hang of it, but how would I approach building an app that has say 4 teams (orange, blue, green, yellow) then 5-8 players per team?
B. Describing the code: Below is the code. I have a App.Models.Player model, an App.Views.HomePlayer and App.Views.AwayPlayer view, then there is a App.Collections.PlayersList collection, then a view for the collection App.Views.Players which displays each player model as a list, I render the same collection players = new App.Collections.PlayersList inside the home player view and an away player view. I am not sure how to associate them to a team? Do I create two collections, I don't want to create too many collections or too many views, I'm trying to figure out how to make this as minimal and readable as possible, I need some direction :) !
C. Requirements: Nothing too fancy here, I need a list of 4 teams, then 5-8 players per team(total of about 20-25 players, then need to associate them to a specific team) and I need to identify the current selected teams as either home or away. The home teams and away teams are interchangeable, I was thinking of having a list of the same teams on both side and hiding them, then the current teams that are playing will be displayed, I am just not sure if there is a better way than that?
Final Word: Yes I am using window for testing in chrome's console.log.. it will be removed when I get some momentum going.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>You have arrived.</h1>
<div class="app">
<button type="button"class="add">ADD</button>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="http://underscorejs.org/underscore.js"></script>
<script src="http://backbonejs.org/backbone.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone-localstorage.js/1.1.0/backbone.localStorage-min.js"></script>
<!-- Templates -->
<script type="text/template" id="home-template">
<div style="float: left; width: 47%;">Name: <%= name %> - Points: <%= points %><button class="btn"></button></div>
</script>
<!-- Templates -->
<script type="text/template" id="away-template">
<div style="float: right; width: 47%;">Name: <%= name %> - Points: <%= points %><button class="btn"></button></div>
</script>
<script>
$(function(){
//Name spacing
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
/*** OUR MODEL OF A PLAYER... PLAYER MODEL then SINGLE PLAYER VIEW ***/
// Player Model
// ----------
// Our **Player** model has `name`, `points`, and `rebounds` attributes.
window.App.Models.Player = Backbone.Model.extend({
// Default attributes for the player item.
defaults: {
name: "Michael",
points: 10,
rebounds: 9
}
});
// Single player view
// ---------------
// This is a view of how a player should look.
window.App.Views.HomePlayer = Backbone.View.extend({
//el is a list tag.
tagName: "li",
// Cache the template function for a single item.
template: _.template($('#home-template').html()),
events: {
'click .btn': 'mikeAlert'
},
mikeAlert: function() {
alert('get food');
},
// Re-render the titles of the todo item.
render: function() {
this.$el.html( this.template( this.model.toJSON() ) );
return this;
}
});
// Single player view
// ---------------
// This is a view of how a player should look.
window.App.Views.AwayPlayer = Backbone.View.extend({
//el is a list tag.
tagName: "li",
// Cache the template function for a single item.
template: _.template($('#away-template').html()),
events: {
'click .btn': 'mikeAlert'
},
mikeAlert: function() {
alert('get food');
},
// Re-render the titles of the todo item.
render: function() {
this.$el.html( this.template( this.model.toJSON() ) );
return this;
}
});
/*** END PLAYER MODEL SETUP ***/
/*** OUR PLAYERS COLLECTION... PLAYERS COLLECTION then PLAYERS COLLECTION VIEW ***/
// Players Collection
// ---------------
// We connect the players collection to the player model
// The collection of players is backed by *localStorage* instead of a remote
// server.
window.App.Collections.PlayersList = Backbone.Collection.extend({
// Reference to this collection's model.
model: App.Models.Player
// Save all of the player items under the `"players-backbone"` namespace.
//localStorage: new Backbone.LocalStorage("players-backbone"),
});
// Players Collection View
// ---------------
// Display a list of all player*s* here.
window.App.Views.Players = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $(".app"),
initialize: function() {
this.render();
},
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(model) {
//Create a new child view
var homeplayer = new App.Views.HomePlayer({ model: model });
var awayplayer = new App.Views.AwayPlayer({ model: model });
//Then append it to the root, this
this.$el.append( homeplayer.render().el );
this.$el.append( awayplayer.render().el );
}
});
/*** END PLAYER*S* COLLECTION SETUP ***/
// Dummy Collection, new instance of *App.Collections.PlayersList*
// ---------------
window.players = new App.Collections.PlayersList([
{
name: 'McGee',
points: '14'
},
{
name: 'Joe E',
points: '21'
},
{
name: 'Mike',
points: '8'
}
]);
//Create new instaces to initialize each view
// New *App.Views.Player* instance, need to instantiate to set the model in the view.
// ------------
window.playersView = new App.Views.Players({ collection: players });
});
</script>
</body>
</html>
Please ask any questions, it would be so cool to get some insight and finally working on a real world project, this could be fun for most and easy points!
THANKS!!
EDIT updated array: could I re arrange my array? if so, this changes the model, is this valid, how do I implement this? Any links.. advice this would be cool, I wouldnt have to create a new collection I wouldnt think. AHhh really new to this stuff so any tips would be comforting.
{
"team":
{
"blue":
[
{ "name": "Mike", "points": 10 },
{ "name": "Joe", "points": 13 },
{ "name": "Kobe", "points": 23 }
]
},
},
{
"team":
{
"orange":
[
{ "name": "John", "points": 12 },
{ "name": "Narlens", "points": 33 },
{ "name": "MJ", "points": 22 }
]
},
}
So I guess instead of creating a new collection/model etc I can just add that players team name then add the team name inside a class and only show that specific team and it's players.. here is the jSON array. Please if there is a better way, explain I am learning and got into backbone to have a better developer tools.
window.players = new App.Collections.PlayersList(
[
{
team: 'Green',
name: 'McGee',
points: '14'
},
{
team: 'Green',
name: 'Joe E',
points: '21'
},
{
team: 'Green',
name: 'Mike',
points: '8'
},
{
team: 'Blue',
name: 'Eli',
points: '14'
},
{
team: 'Blue',
name: 'Michael',
points: '21'
},
{
team: 'Blue',
name: 'Tim',
points: '8'
}
]
);
EDIT
Added the jsfiddle:
http://jsfiddle.net/ythLm/
Not sure if that's what you wanted to implement. I am assigning players to teams in this demo, which may or may not satisfy all your future requirements. (but we can always improve) :)
===ORIGINAL ANSWER===
I'm not sure I fully understand your requirements here...so you wanna display all the players in one view? and show their team names and sides? what's the order for the players list?
but:
to assign players to teams, you can just simply pass them as an attribute on the team model.
Oh yea, I think You need a team model:
App.Models.Team = Backbone.Model.extend({
//team stuff
});
App.Collections.Teams = ... //usually you want a collection too
and then
var blueTeam = new App.Models.Team({
name: 'blue team',
players: new App.Collection.Players([
{"name": "Mike", "points": 10 },
{ "name": "Joe", "points": 13 },
{ "name": "Kobe", "points": 23 }
]);
});
Home team and away team, or just currently selected team, can just be an attribute on the team model. When you want to change them, you can just change the attribute on the team model. eg.
var blueTeam = new App.Models.Team({
//other attributes and players
selected: true,
side: 'home'
});
You can attach those status to collections as well, but I don't recommend doing that. (let's think if this is a real world application, Team has_many :players, and Player belongs_to :team)
but you want to have all the players in one list (if i understand your requirements correctly), so we need to assign Team to each Player, instead of assigning players to a Team:
var mike = new App.Models.Player({
name: 'mike',
team: new App.Models.Team({
name: 'blue'
});
});
but there's a problem with this: there are 20 players, but only 4 teams, you will have each team repeated 5 times, and they are all different objects. eg. if you do
mike.get('team').set('selected', true);
only mike's 'team' will be selected, other blue team players' "team" will not be updated.
to solve this.
First, get all the players altogether. remember, they need to a foreign key, or any kind of reference to the team they belong to: (it could be team_id, or just team_name)
var players = App.Collections.PlayersList([
{"name": "Mike", "points": 10, team_id: 1},
{"name": "Joe", "points": 13, team_id: 2},
{"name": "Kobe", "points": 23, team_id: 1}
//... all your players
]);
and then, your teams, but you don't have to assign players :
var teams = new App.Collections.Teams([
{"id": 1, name: "blue team", selected: false, side: ''},
{"id": 2, name: "red team", selected: false, side: ''}
//... all your teams
]);
now, somewhere in your app assign teams to your players:
players.each(function(player){
player.set("team", teams.get(player.get("team_id")));
});
doing it this way, makes sure all the players on the same team will have the same team model.
(Example http://jsfiddle.net/sbjaz/10/ when the code is paused by debugger, open your console, and try to play with player_1, player_2, and player_3, 1 and 2 are on the same team, try change any attributes on the team model for player1/2 and check the other one's team model)
(another Example http://jsfiddle.net/sbjaz/11/ do the same, you will notice when you update the team for player_1, player_2's team is not updated)
finally, in your collection view:
addOne: function(model) {
var playerView;
if(model.get("team").isHomeTeam()) { //isHomeTeam is a helper method in team model
playerView = new App.Views.HomePlayer({ model: model });
} else {
playerView = new App.Views.AwayPlayer({ model: model });
}
this.$el.append( playerView.render().el );
}
well, and you need to pass Teams collection to PlayerList view as well so you know if any of the team has changed:
new playersView = new App.Views.Players({
collection: players,
teams: teams
});
track team changes, in your view definition:
App.Views.Players = Backbone.View.extend({
initialize: function(options) {
this.teams = options.teams;
this.teams.on('change', this.render, this); //if any of the team changes, rerender everyting
this.render();
}
});
and, you may think, this is not efficient at all: each time you swtich Home/Away teams, all the players will have to be re-rendered. how do I refactor this?
well first, I think you don't have to define two views for the player view -- they have too much repeated code! it's not DRY.
let's define a view that works for any player (at least for now):
App.Views.PlayerView = Backbone.View.extend({
tagName: "li",
// events...mikeAlert they are all the same
// you will see why we assign this.template in render instead of initialize
//well or you don't have to assign this.template at all.
render: function() {
var templateSelector = this.model.isHomeTeam() ? "#home-template" : "#away-template";
this.template = _.template($(templateSelector).html());
return this.$el.html(this.template(this.model.toJSON());;
}
});
you get the idea, or may be you don't even have to write two templates if they have too much repeated code too!
well you need to update addOne:
//since PlayerView's render() returns html directly...
addOne: function(model) {
this.$el.append(new App.Views.PlayerView({model: model}).render());
}
now events, you don't have to pass teams to your PlayersView or bind collection change events there anymore.
in your PlayerView's initialize:
initialize: function() {
//rerender player view, when team is changed!
this.model.on('change', this.render, this);
}
now, every time you change any team, only the relevant playerViews will be updated.

"this.collection.each is not a function". Shouldn't it simply say "each"?

I really have a two part question.
The console is telling me: "TypeError: this.collection.each is not a function"
In the short-term I would love to know why my code isn't working.
In the long term I am more interested in knowing why it is not telling me that "each" is not a function, since that is the method I am trying to call.
P.S. I have confirmed that JQuery is loading correctly, and before this code loads, so that is not the problem.
The pertinent javascript is:
$(function(){
var items = [
{ name: 'Abe Lincoln', details: 'Is he that guy from that new reality show?'},
{ name: 'Captain Planet', details: 'He is our hero'},
{ name: 'Karthus', details: 'Press R'},
{ name: 'Your Mom', details: 'She misses me'},
{ name: 'Teddy Roosevelt', details: 'Makes the most interesting man in the world look boring'}
];
var itemsCollectionView = new ListView({collection: items});
Backbone.history.start();
});
var ListView = Backbone.View.extend({
el: '#the-list',
initialize: function(){
this.render();
},
render: function(){
this.collection.each(function(model){
this.addOne(model);
}, this);
},
//create an itemview for a model, and add it to the list view
addOne:function(model){
var itemView = new ItemView({model: model});
this.$el.append(itemView.render().el);
}
});
this.collection.each is fine to use with Backbone, the problem is that you are not passing an actual Backbone collection to the instance of ItemView, but simply an array. You would need something like the following:
var itemsCollection = new ItemsCollection(items), // substitute your collection variable
itemsCollectionView = new ListView({ collection: itemsCollection });
Also, I tried running your code on Backbone 1.0.0 and jQuery 1.10.1 and I get
TypeError: Object [object Array] has no method 'each'

best way to realize backbone list application

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/

Categories

Resources