Backbone / Marionette ItemView strategy - backbone.js

I'm in the process of setting up a website with a blog that has some peculiarities. It's sort of a tumblr-esque experience, where there's different post-type:
Facebook-post
Quote
Article
Research
These posts share some common attributes such as id, title, post_date, post_url_slug, but some have a post_image or post_external_link for example. This is all dependent on the post_type, which can hold values such as facebook, quote, article etc. What would be a good strategy to determine which type it is when rendering a Marionette.CollectionView and either selecting a different tempalte altogether or handling this in the template with underscore's arbitrary javascript in my template? Any input would be appreciated.
Thanks!

Definitely don't put the logic in the view template. That will lead to pain, suffering, hate and the dark side. :P
You can override the getItemView method on the CollectionView or CompositeView and use that to determine which view type you want. https://github.com/marionettejs/backbone.marionette/blob/master/src/marionette.collectionview.js#L122
var Stuff = Marionette.CollectionView.extend({
// ...,
getItemView: function(item){
var type = item.get("whatever_field");
var viewType;
if (type === "some value"){
viewType = SomeViewType;
} else {
viewType = AnotherViewType;
}
return viewType;
}
});

You should have one single ItemView, don't need to override getItemView of the CollectionView. The ItemView should be the one in charge of deciding the template to use, that's what getTemplate is for. It should look like this:
var MyItemView = Marionette.ItemView.extend({
getTemplate: function() {
var template;
switch(this.model.get('type')) {
case 'facebook':
template = 'Template for Facebook type';
break;
case 'quote':
template = 'Template for Quote type';
break;
case 'article':
template = 'Template for Article type';
break;
}
return template;
}
});
This way you only have one ItemView for the one model you have and the rendering decisions are left to the template as it should be.

You could handle this in the ItemView intialize function and switch the template there. So long as you are planning to use the same events cross post type

Related

Extending Marionette view

In my application, I found it handy to implement a FrameView that would extend LayoutView but would have some additional logic for keeping frame title etc. (my goal is to let Regions deal with displaying the title).
From the Java's Swing perspective, LayoutView is a JPanel for me, but I would like to turn it to a JFrame wannabe :-)
Is there a way to extend the Marionette view (and perform some initialization) in such a way, that the initialize method remains unused? (I don't want to bother user with calling FrameView.prototype.initialize method to make the things work)
This is my attempt (but it suffers from the problem mentioned above):
var FrameView = Marionette.LayoutView.extend({
initialize: function() {
if(!this.title) this.title = null;
},
setTitle: function(title) {
this.title = title;
this.trigger("change:title", title);
}
})
Ideally the FrameView would combine LayoutView with Backbone.Model (so I would get those methods like setTitle for granted, even though with slightly different syntax). The Backbone.Model part of the view would than keep things like title, icon, whatever.
I am still learning both Backbone.js and Marionette, so my way of thinking might be odd. I would be thankful for both your answers and for any recommendation how to achieve my goal.
If you simply want a default title key on your view object, then extend your view prototype with the key:
var FrameView = Marionette.LayoutView.extend({
title: null,
setTitle: function(title) {
this.title = title;
this.trigger("change:title", title);
}
})

BackboneJS - same el for many views

I am using same el for more than 1 view like below. I'm not facing any problem till now. Is this good approach or should i do any changes?
<div id="app">
<div id="app-header"></div>
<div id="app-container"></div>
<div id="app-footer">
</div>
App View:
{
el: "#app",
v1: new View1(),
v2: new View2(),
render: function () {
if (cond1) {
this.v1.render();
} else if (cond2) {
this.v2.render();
}}
}
View 1:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
View 2:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
By reading your question, I do not really see what advantages you could possibly have using this approach rather than having the different div elements being the root el for your views 1, 2, 3 and using
this.$el.html(template)
in the render method.
Your approach could work for a small application, but I think it will become really hard to maintain as the application grows.
EDIT
I still do not really get your point, you could only initialize everything only once in both cases.
Here is a working Fiddle.
By the way I am changing the content by listening to the click event but this is to simplify the example. It should be done by the router.
I do use a mixin to handle such situation, I call it stated view. For a view with all other options I will send a parameter called 'state', render will in-turn call renderState first time and there after every time I set a 'state' renderState will update the view state. Here is my mixin code looks like.
var setupStateEvents = function (context) {
var stateConfigs = context.getOption('states');
if (!stateConfigs) {
return;
}
var state;
var statedView;
var cleanUpState = function () {
if (statedView) {
statedView.remove();
}
};
var renderState = function (StateView) {
statedView = util.createView({
View: StateView,
model: context.model,
parentEl: context.$('.state-view'),
parentView:context
});
};
context.setState = function (toState) {
if (typeof toState === 'string') {
if (state === toState) {
return;
}
state = toState;
var StateView = stateConfigs[toState];
if (StateView) {
cleanUpState();
renderState(StateView);
} else {
throw new Error('Invalid State');
}
} else {
throw new Error('state should be a string');
}
};
context.getState = function () {
return state;
};
context.removeReferences(function(){
stateConfigs = null;
state=null;
statedView=null;
context=null;
})
};
full code can be seen here
https://github.com/ravihamsa/baseapp/blob/master/js/base/view.js
hope this helps
Backbone Rule:
When you create an instance of a view, it'll bind all events to el if
it was assigned, else view creates and assigns an empty div as el for that view and bind
all events to that view.
In my case, if i assign #app-container to view 1 and view 2 as el and when i initialize both views like below in App View, all events bind to the same container (i.e #app-container)
this.v1 = new App.View1();
this.v2 = new App.View2();
Will it lead to any memory leaks / Zombies?
No way. No way. Because ultimately you are having only one instance for each view. So this won't cause any memory leaks.
Where does it become problematic?
When your app grows, it is very common to use same id for a tag in both views. For example, you may have button with an id btn-save in both view's template. So when you bind btn-save in both views and when you click button in any one the view, it will trigger both views save method.
See this jsFiddle. This'll explain this case.
Can i use same el for both view?
It is up to you. If you avoid binding events based on same id or class name in both views, you won't have any problem. But you can avoid using same id but it's so complex to avoid same class names in both views.
So for me, it looks #Daniel Perez answer is more promising. So i'm going to use his approach.

Extending Backbone Collections to add logic, with custom methods, is a bad practice?

Usually I find my self needing to write an object with a specific functionality that it is a set of models.
Finally I extend a collection and add more functions that works with its model.
I think is better show you an example:
My app has a set of permissions, related with the user and/or the version of the platform.
var Permissions = Backbone.Collection.extend({
model: Permission,
hasAccess: function (moduleCode) {
....
},
allowAccess: function (moduleCode) {
....
},
...
With that methods I change the format of a property of a permission (the model). (My permissions are a concatenation of code with an string that identify the type of permission.)
A workmate tells me that it is wrong. In .NET he creates a class and he adds a private list and makes the changes to it. He does not inherit the list and changes it.
He would make a model and inside it he would add a collection property
this.set("permissionsCollection", new Backbone.Collection.extend({model: Permission}))
[Comment: I don't understand why he creates properties of everything, I think in his case it is not needed.] -> But this is another question
I think in a different way. I know the Collection has internally that list. I have a great potencial in Backbone.Collections, why do I have to use a model that it is not necessary? If I don't need that encapsulation... I think that it is not necessary, he is overprogramming in my opinnion.
Am I wrong? Did I not know how to use BackboneJS Collections?
Thank you in advance.
At the beginning I had something called helper with similar methods:
findAttr: function (model, name) {
var attrs = model.get('app_attrs');
if (attrs !== undefined) {
return this.findByAttrName(attrs, name);
}
},
findByAttrName: function (array, name) {
return _.find(array, function(a) {
if (a.attrName === name) {
return a;
}
});
}
The view code was more awkward:
render: function () {
var attr = helper.findAttr(this.model, 'user');
...
return this;
}
The only logical solution was to move these methods into the model (this.model in the above case). After refactoring I've got:
render: function () {
var attr = this.model.findAttr('user');
...
return this;
}
which is of course more readable than the previous solution.

How to display an item view with no tag

The problem is I want to render option elements with the value attribute set as well as the text node.
So setting tagName to option in my ItemView does not just do this. The solution I have at the moment is to set it to option and then use the following code in the ItemView constructor to set the value attibute:
onRender: function () {
this.$el.attr('value', this.model.get('name'));
}
This works.
But is there any other way?
What I'd really like to do is just tell Marionette not to output an element at all and then in my ItemView template have this:
<option value="<%= name %>"> <%= name %> </option>
Is this possible?
It's possible but a bit fiddly, by default Backbone always uses a wrapper element (definable by using tagName) but you'd have to expressly populate the attributes (as you are doing above).
It is however possible to circumvent the wrapper element using a slightly convoluted setElement approach and this will enable you keep all markup in a template with attributes on the root node populated from the model. I like this approach as well since I personally think it keeps a clearer separation of concerns.
Take a look here for an example - Backbone, not "this.el" wrapping
Not sure if Marionette has a build in mechanism for doing this.
I am not sure you should from all different kind of reasons.
But I understand that in the use case of a single tag view, the way Marionette/Backbone work you either create another tag and have a view, or you use onRender and query to update the single tag that was already generated for you.
You could have this Object that will extend ItemView, lets call it SingleTagView. Basicly I am extending ItemView and using overrunning it's render function with one change.
Instead of doing:
this.$el.html(html);
I am doing:
var $html = $(html);
this.$el.replaceWith($html);
this.setElement($html);
Here is the code:
var SingleTagView = Marionette.ItemView.extend({
render: function() {
this.isClosed = false;
this.triggerMethod("before:render", this);
this.triggerMethod("item:before:render", this);
var data = this.serializeData();
data = this.mixinTemplateHelpers(data);
var template = this.getTemplate();
var html = Marionette.Renderer.render(template, data);
// above is standart ItemView code
var $html = $(html);
this.$el.replaceWith($html);
this.setElement($html);
// this code above replaced this.$el.html(html);
// below is standart ItemView code
this.bindUIElements();
this.triggerMethod("render", this);
this.triggerMethod("item:rendered", this);
return this;
}
});
If you are going to use this kind of angle, make sure you test the behavior properly (event handling, Dom leaks, different flows with before:render events).
I would try:
var optionTag = Marionette.ItemView.extend({
tagName: "option",
initialize: function(){
this.attributes = {
value: this.model.get('name');
}
}
}
Backbone should then do the rest (i.e. putting the content of attributes into the HTML tag)...

Add class to elements for values of attribute with Backbone.ModelBinder

I'm using Backbone.ModelBinder in a Backbone.js Marionette project. I've a scenario which I can't work out how to use ModelBinder to automatically update my model/UI.
My model has a 'status' string attribute, with multiple states. In this example I'll show the code for two: 'soon', 'someday'
In my UI I have a list on which I use click events to set the model status, and update classes to highlight the relevant link in the UI.
<dd id="status-soon"><a>Soon</a></dd>
<dd id="status-someday" class="active"><a>Someday</a></dd>
events: {
'click #status-soon': 'setStatusSoon',
'click #status-someday': 'setStatusSomeday'
},
setStatusSoon: function () {
this.model.set('status', 'soon');
this.$el.find('.status dd').removeClass('active');
this.$el.find('#status-soon').addClass('active');
},
... etc
It feels like I doing this a long-winded and clunky way! The code bloat increases with the number of states I need to support. What's the best way of achieving the same outcome with ModelBinder?
You could probably simplify things with a data attribute, something like this:
<dd data-status="soon" class="set-status"><a>Soon</a></dd>
<dd data-status="someday" class="set-status active"><a>Someday</a></dd>
and then:
events: {
'click .set-status': 'setStatus'
},
setStatus: function(ev) {
var $target = $(ev.target);
var status = $target.data('status');
this.model.set('status', status);
this.$el.find('.status dd.set-status').removeClass('active');
$target.addClass('active');
}
You might not need the set-status class, just keying things on the <dd>s might be sufficient; I prefer separating my event handling from the nitty gritty element details though.
Unfortunately, it is going to be pretty difficult to do exactly what you want with ModelBinder. The main reason being that ModelBinder wants to provide the same value for all elements that are part of a single selector. So doing this with ModelBinder, while possible, is going to be pretty verbose as well.
The cleanup offered by mu is likely to be better than trying to use ModelBinder. 1) because you need a click handler to do the this.model.set no matter what and 2) you would need individual bindings for ModelBinder because the converter function is called once for a single selector and then the value is set on all matching elements (rather than looping through each one).
But if you do want to try and do something with ModelBinder it would look something like this:
onRender : function () {
var converter = function (direction, value) {
return (value == "soon" ? "active" : "");
};
var bindings = {
status : {selector : "#status-soon", elAttribute : "class", converter : converter}
};
this.modelBinder.bind(this.model, this.el, bindings);
}
This would do what you want. Of course the down side as I said above is that you will need multiple selector bindings. You could generalize the converter using this.boundEls[0] but you will still need the separate bindings for it to work.
In case you want to access to the bound element, it is possible to declare 'html' as elAttrbute, modify the element and return its html with converter function:
onRender : function () {
var converter = function (direction, value, attribute, model, els) {
return $(els[0]).toggleClass('active', value === 'soon').html();
};
var bindings = {
status : {
selector : "#status-soon",
elAttribute : "html",
converter : converter
}
};
this.modelBinder.bind(this.model, this.el, bindings);
}

Resources