Is there a way to change Marionette ItemView template dynamically with RequireJS? - backbone.js

I'm trying to manipulate itemViews dynamically in a Marionette CollectionView. The collections have the same models, but i defined templateName argument inside the models.
The question is, can i manipulate the ItemView template by this argument?
ItemView:
define(['text!templates/ComponentItemViewTemplate.html','models/ComponentModel'], function (template, model) {
var ItemView = Backbone.Marionette.ItemView.extend({
template: _.template(template),
model: model
});
return ItemView;
});
CollectionView:
define(['views/ComponentItemView', 'views/LoadingView'], function(ItemView, LoadingView) {
var ComponentListView = Backbone.Marionette.CollectionView.extend({
emptyView : LoadingView,
id: "component-list",
itemView: ItemView,
events: {
'click .title span' : 'show'
},
appendHtml: function(collectionView, itemView, index){//i would like to render different templates, for different models.
itemView.$el.draggable({ helper: "clone", cancel: ".component .title span", connectToSortable: ".ui-sortable" });
collectionView.$el.append(itemView.el);
},
show: function(r) {
var target = $(r.target);
if( target.parent().hasClass('open') ){
target.parent().removeClass('open');
target.parent().next().slideDown('fast');
}else{
target.parent().addClass('open');
target.parent().next().slideUp('fast');
}
}
});
return ComponentListView;
});
Thanks!

You can override getTemplate function and write your custom logic there. The Marionette documentation recommends the following option:
MyView = Backbone.Marionette.ItemView.extend({
getTemplate: function(){
if (this.model.get("foo")){
return "#some-template";
} else {
return "#a-different-template";
}
}
});

I think gumballhead is on the right track. You can override the getTemplate function to do this.
MyCollectionView = Marionette.CollectionView.extend({
// ...
getItemView: function(item){
// get the template from the item... or wherever else it comes from
return new MyViewType({
template: item.get("the-template")
});
}
});
Hope that does what you need

First of all i'd like to thanks for everybody who tried to help me.
I resolved my own problem.
Here is the sollution, if somebody need it:
define(['models/ComponentModel'], function (model) {
var ItemView = Backbone.Marionette.ItemView.extend({
model: model,
render: function() {
var that = this;
var data = this.serializeData();
require(['text!templates/components/editor/' + that.model.get('editor_template') + '.html'], function(Template){
var html = _.template(Template, data);
that.$el.html(html);
});
}
});
return ItemView;
});
edited: (Better sollution)
Suggestions are welcome!

Related

BackboneJS - How to add a Progressbar while fetching collection

I have an Backbone App where I fetch different collections by clicking a Letter from a list. So, I want to add a Progressbar or some kind of rotating image but I dont know how to do this.
My View looks like this
function (App, Backbone) {
var Artists = App.module();
var ArtistView = Backbone.View.extend({
tagName : 'li',
template: 'artistItem',
serialize: function() {
var data = this.model.toJSON();
data.letter = this.model.collection.letter;
return data;
},
});
Artists.View = Backbone.View.extend({
tagName : 'ul',
className : 'artistList',
initialize: function() {
this.listenTo(this.collection, 'all', this.render);
this.listenTo(App, 'navigateLetter', this.updateState);
},
beforeRender: function() {
var self = this;
this.collection.each(function(item) {
self.insertView(new ArtistView({model: item}))
})
},
updateState: function(letter) {
this.collection.letter = letter;
this.stopListening(this.collection);
this.collection.fetch();
this.listenTo(this.collection, 'all', this.render);
}
});
Artists.ArtistsCollection = Backbone.Collection.extend({
url: function() {
return '/projects/mdk/index.php/api/artists/' + this.letter;
}
});
return Artists;
});
So does anyone have an idea how to do this? I could imagine I should do something in initialize or beforeRender?
Thanks in advance
You can use a spinner for the loading effect. For that you need
spin.js
Add entry of that spin.js into main file.
To use that spinner.
var yourSpinner = new Spinner();
var target = document.getElementById('spinHere');
yourSpinner.spin(target);
e.g in your case take updateState:function(){} :
updateState: function(letter) {
this.collection.letter = letter;
this.stopListening(this.collection);
var yourSpinner = new Spinner();
var target = document.getElementById('spinHere');
yourSpinner.spin(target);
this.collection.fetch();
yourSpinner.stop();
this.listenTo(this.collection, 'all', this.render);
}
Take a look at this: https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/apps/contacts/list/list_controller.js#L4
It's from my Marionette book app, where the idea is to immediately display a loading view, and when the collection is fetched, render the actual view (and closing the loading view, which is Handled by Marionette). It would give something like (pseudocode):
var loadingView = new ContactManager.Common.Views.Loading();
ContactManager.mainRegion.show(loadingView);
var fetchingContacts = myCollection.fetch();
$.when(fetchingContacts).done(function(contacts){
ContactManager.mainRegion.show(new MyCollView({ collection: contacts }));
});
The code uses a deferred to determine when the collection has been fetched (and therefore the new view should be displayed). You can learn more about using deferreds here:
http://davidsulc.com/blog/2013/04/01/using-jquery-promises-to-render-backbone-views-after-fetching-data/
http://davidsulc.com/blog/2013/04/02/rendering-a-view-after-multiple-async-functions-return-using-promises/

How to hook async Backbone event to display of HTML

What I am trying to do is make a call to the database and then display the result in some HTML. I have everything working (the data comes back from the database just fine), except I can't figure out to display the data.
I know that fetch() is async, but I'm not sure how to wire it into my collection view. Here is my Backbone:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
App.Models.Main = Backbone.Model.extend({
defaults : {
FName: ''
}
});
App.Collections.Mains = Backbone.Collection.extend({
model: App.Models.Main,
initialize: function(mains) {
this.fetch({success: function(main) {
$('#web-leads').html(main);
}});
},
url: '../leads/main_contact'
});
App.Views.Mains = Backbone.View.extend({
tagName: 'ul',
render: function() {
var ul = this.collection.each(this.addOne, this);
return ul;
},
addOne: function(main) {
var mainC = new App.Views.Main({ model: main});
this.$el.append(mainC.render().el);
return this;
}
});
App.Views.Main = Backbone.View.extend({
tagName: 'li',
template: template('mainContactTemplate'),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
main = new App.Views.Main();
mains = new App.Collections.Mains(main);
})();
What I need to be able to is call $('#web-leads').html() with the value returned from mains. How do I do that?
The general pattern for this sort of thing in Backbone is:
create a model or collection
pass that model/colleciton to a view
that view registers an event handler on the model/collection
the model/collection triggers an AJAX request (probably in response to a fetch call)
the view's event handler is triggered
the view's event handler updates the page
So, as mu is too short suggested, your best bet is to follow this pattern and have your view bind a handler to your collection's reset event.
It's worth mentioning however that reset won't always be the event you want to bind. For instance, you might not want to respond an AJAX request unless it changed attribute 'X' of the model. In that case you could instead bind to change:X, and then your handler would only be triggered if the AJAX response changed X.
To see all your possible options, see:
http://documentcloud.github.com/backbone/#Events-catalog
You were on the right track just needed to have the view listening to the Collection rather than the collection listening to the view.
The below is your code with the slight modification of who listens to who.
Why? Ideally we want the Collections to know nothing of the Views.
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
App.Models.Main = Backbone.Model.extend({
defaults : {
FName: ''
}
});
App.Collections.Mains = Backbone.Collection.extend({
model: App.Models.Main,
url: '../leads/main_contact'
});
App.Views.Mains = Backbone.View.extend({
tagName: 'ul',
initialize : function(){
this.collection.on('reset', this.render, this);
},
render: function() {
var ul = this.collection.each(this.addOne, this);
return ul;
},
addOne: function(main) {
var mainC = new App.Views.Main({ model: main});
this.$el.append(mainC.render().el);
return this;
}
});
App.Views.Main = Backbone.View.extend({
tagName: 'li',
template: template('mainContactTemplate'),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
mains = new App.Collections.Mains();
main = new App.Views.Main( {'collection' : mains} );
mains.fetch();
})();

How to pass a template to a view in Backbone

I'm trying to pass a template to my view. I have several different templates I want to use and want to be able to switch them up in my router. I get no errors, but I get no results. It looks like the initialize method isn't being called in my second view. Here is my code:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
var vent = _.extend({}, Backbone.Events);
_.templateSettings.interpolate = /\[\[(.+?)\]\]/g;
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'send-message' : 'sendMessage',
'*other' : 'other'
},
index: function() {
t = new (App.Collections.Tables.extend({ url: 'main-contact'}))();
tables = new (App.Views.Tables.extend({
collection: t, template: template('mainContactTemplate')}))();
$('#web-leads').html(tables.el);
},
sendMessage: function() {
t = new (App.Collections.Tables.extend({ url: 'send-message'}))();
tables = new App.Views.Tables.extend({
collection: t, template: template('sendMessageTemplate')});
$('#web-leads').html(tables.el);
},
other: function() {
}
});
// Main Contact
App.Models.Table = Backbone.Model.extend({});
App.Collections.Tables = Backbone.Collection.extend({
model: App.Models.Table,
initialize: function(models, options) {
this.fetch({
success: function(data) {
//console.log(data.models);
}
});
if (options) {
this.url = this.url || options.url;
}
}
});
App.Views.Tables = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('reset', this.render, this);
},
render: function() {
return this.collection.each(this.addOne, this);
},
addOne: function(model) {
var t = new App.Views.Table({ model: model, template: template});
this.$el.append(t.render().el);
return this;
}
});
App.Views.Table = Backbone.View.extend({
tagName: 'li',
template: this.template,
initialize: function (attrs) {
this.options = attrs;
console.log(this.options);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
new App.Router();
Backbone.history.start();
})();
EDIT: I was missing some parenthesis. But now I get an error of an unrecognized expression. Initialize is now being called.
The way you are doing it in App.Views.Table is (as far as I can tell) the "standard" way of using templates with Backbone. There are of course several alternatives though, and none of them are "wrong" per say.
That being said, you do have a couple problems in your code. Let's start with:
template: this.template,
At the time that code runs you're not in an instance of App.Views.Tables, you're in the global space declaring a class that (later) will be used to make instances. At that moment though, this just refers to window. What you really want to do is set the template in your initialize, which leads me to:
initialize: function(options) {
this.template = options.template;
},
But then there's one last problem:
var t = new App.Views.Table({ model: model, template: template});
there is no template variable in that function, so you're really doing template: undefined. That should use a real template.
All that being said, you might want to just consider putting the template on the view directly, the way you sort of tried to:
template: Handlebars.compile('<span>{{test}}</span>'),
After all, any given view should always use the same template, right? Also, you might want to consider moving the:
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
in to a parent class, so that you can share it between all of your templated views, instead of having to repeat it.

Binding data to the DOM in Backbone

Here's my Backbone.js:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
var vent = _.extend({}, Backbone.Events);
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'*other' : 'other'
},
index: function() {
},
other: function() {
}
});
App.Models.Main = Backbone.Model.extend({
defaults : {
FName: ''
}
});
App.Collections.Mains = Backbone.Collection.extend({
model: App.Models.Main,
initialize: function() {
this.fetch({
success: function(data) {
console.log(data.models);
}
});
},
url: '../leads/main_contact'
});
App.Views.Mains = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('reset', this.render, this);
console.log(this.collection);
},
render: function() {
return this.collection.each(this.addOne, this);
},
addOne: function(main) {
var mainC = new App.Views.Main({ model: main});
this.$el.append(mainC.render().el);
return this;
}
});
App.Views.Main = Backbone.View.extend({
tagName: 'li',
template: template('mainContactTemplate'),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
mains = new App.Collections.Mains();
main = new App.Views.Main({ collection: mains});
new App.Router;
Backbone.history.start();
})();
What I want to do is have the data returned in the ul to be bound to a DOM element called $('#web-leads'). How do I do that, given this code? Incidentally, I've already posted about this here and tried to follow the first answer and the second answer combined. But I still don't get HTML and data bound to the DOM. The data is returning from the server correctly in my collection, so I know that's not the problem. Don't worry about the router stuff. That's for later.
Generally in Backbone you don't put data on DOM elements: you put it in views that wrap that DOM element.
That being said, if you really want to store data on the element, jQuery has a function for that:
$('#web-leads').data('someKey', 'yourData');
Then you can retrieve that data with:
$('#web-leads').data('someKey');
* EDIT *
In a comments discussion with the OP it became apparent that the real goal was simply to append a view's element to an element on the page. If the element being appended to is #web-leads, then this can be accomplished with:
$('#web-leads').append(theView.render().el);

Backbone.js - Correct way of filtering and displaying collection data in a view

I have got a huge list of tasks loaded on the start.
I want to show them depending on selected list / inbox, so that there won't be additional loadings for each list.
window.Task = Backbone.Model.extend({});
window.TasksCollection = Backbone.Collection.extend({
model: Task,
url: '/api/tasks',
inbox: function() {
return this.filter(function(task) {
return task.get('list') == null;
});
},
list: function(id) {
return this.filter(function(task) {
return task.get('list') == id;
});
}
});
window.tasks = new TasksCollection;
window.TaskView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#item-template').html()),
initialize: function() {
_.bindAll(this, 'render', 'close');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
this.setContent();
return this;
},
});
window.TasksView = Backbone.View.extend({
el: '#todo-list',
collection: tasks,
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
var t = this;
$(t.el).html('');
this.collection.each(function(task) {
var view = new TaskView({ model:task });
$(t.el).append( view.render().el );
});
return this;
},
});
window.Nicetask = Backbone.Router.extend({
routes: {
'': 'inbox',
'/inbox': 'inbox',
'/list/:id': 'list',
},
initialize: function() {
_.bindAll(this, 'inbox', 'list');
window.tasksView = new TasksView;
},
inbox: function() {
tasks.reset( tasks.inbox() );
},
list: function(id) {
tasks.reset( tasks.list(id) );
}
});
This code works, but the reset() function removes other tasks in actual list from tasks collection. And on another route, tasks collection is empty.
Is there any reasonable way to achieve this? thanks for any idea.
ps: backbone novice
UPDATE
Thx to #sled and #ibjhb for comments, here is snippet of working solution.
window.TasksView = Backbone.View.extend({
el: '#todo-list',
collection: Backbone.Collection.extend(),
initialize: function() {
_.bindAll(this, 'render', 'addOne', 'addAll');
this.collection.bind('add', this.addOne);
this.collection.bind('reset', this.render);
},
render: function(data) {
$(this.el).html('');
_.each(data, function(task) {
this.addOne(task);
}, this);
return this;
},
addOne: function(task) {
var view = new TaskView({ model:task });
$(this.el).append( view.render().el );
},
});
window.Nicetask = Backbone.Router.extend({
routes: {
'': 'inbox',
'/inbox': 'inbox',
'/today': 'today',
'/list/:id': 'list',
},
initialize: function() {
_.bindAll(this, 'inbox', 'today');
window.tasksView = new TasksView;
window.menuView = new MenuListView;
tasks.fetch();
},
inbox: function() {
tasksView.render( tasks.inbox() );
},
today: function() {
tasksView.render( tasks.today() );
},
list: function(id) {
tasksView.render( tasks.list(id) );
}
});
I think you need to use another collection. For example, in your inbox, do this:
inbox: function(){
currentCollection = new TasksCollection(tasks.inbox());
}
I haven't tested this but when you do a .reset(); you are removing all your models and loading the ones passed in.
#sled there's typos in the code you posted, see comments inline. Did you post this as a project somewhere?
// add models
add: function(models, options) {
// TYPO: next line was missing, so single models not handled.
models = _.isArray(models) ? models.slice() : [models];
var self = this;
models = _.filter(models, this.filter);
// return if no models exist
// TYPO: returned undefined, so was not chainable
if(models.length == 0) { return this; }
// actually add the models to the superset
this.superset.add(models, options);
return this;
},
// remove models
remove: function(models, options) {
// TYPO: next line was missing, so single models not handled.
models = _.isArray(models) ? models.slice() : [models];
// remove model from superset
this.superset.remove(_.filter(_.filter(models, function(cm) {
// TYPO: not 'm != null', causes error to be thrown
return cm != null;
}), this.filter), options);
// TYPO: missing return so not chainable
return this;
},
one quick amendment to you solution, you are using
$(this.el).html('');
My understanding is your the views and related event bindings will still exist in the browser memory, so you ideally need to use view.remove() on the TaskView to correctly clear the event bindings as well as the html.
This is a slightly different take on the answer as I have been looking for a solution to a similar problem, hope this may be of help to others.
My problem: - to filter a complete collection by attributes of the model. eg. a user clicks the models view, gets a list of (some of) the attributes, selecting an attribute filters the collection to only show ones with the same value.
The route I am taking is by calling a method on the collection from the view, in my case the view is specific to a model so:
this.model.collection.myFilter(attr,val);
where attr is an attribute of the model associated with the collection, then in the filter something like
myFilter: function(attr, val){
var groupByAttr = this.groupBy(function(article){
var res = (val === undefined)? true : (article.get(attr) == val);
article.set({selected:res});
return res;
});
return groupByAttr;
}
I have used ._groupBy as this returns 2 arrays (positive / negative) that may be of use. By setting the mode attribute "selected", and binding to this in the model view I can easily toggle a class which shows or hides the view.
if(val === undefined) is added as a simple way of clearing a filter by calling the same method without a value.

Resources