Extending Marionette view - backbone.js

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);
}
})

Related

Backbone.js How to make collection only accept one class of model

I am new to backbone.js and I am trying to learn it. In the code below I want my collection called "JokesCollection" to only accept adding models of the class "Joke". How do I do achieve this? When setting "Collection" attribute "model" to a certain model, isn´t the collection supposed to only accept that model class and ensure homogeneity? Don´t seam so. When I assign attribute "model" in the "JokesCollection" class to "Joke" it still accepts adding models of class "Persson" witch is not what I want. I only want it to accept adding models of class "Joke".
Joke = Backbone.Model.extend ({
initialize: function(){
console.log("Joke was created");
},
defaults: {
joke : "",
date : "0",
}
});
JokesCollection = Backbone.Collection.extend({
initialize: function(){
console.log("JokesCollection was created");
},
model: Joke // <=== Isn´t this supposed to ensure that the collection only accepts models of class "Joke"?
});
Person = Backbone.Model.extend ({
initialize: function(){
console.log("Person was created");
},
defaults: {
username: "default",
password: "default",
email: "default"
}
});
var person1 = new Person({username:"masterMind"});
var joke1 = new Joke({joke:"Girls are cute and funny hahahaha"});
jokesCollection = new JokesCollection();
jokesCollection.add(joke1);
jokesCollection.add(person1); // This adds a model of class "Person" to the collection. Witch is not what I want. It is not supposed to work! I want "jokesCollection" to only accept models of class "Joke".
console.log(jokesCollection.length); // length gets increased by 1 after adding "person1" to "jokesCollection". Again, it is no supposed to work from my point of view. I want "jokesCollection" to only accept models of class "Joke".
console.log(jokesCollection);
From official docs:
model collection.model
Override this property to specify the model class that the collection
contains. If defined, you can pass raw attributes objects (and arrays)
to add, create, and reset, and the attributes will be converted into a
model of the proper type.
Looks like will have to re-write add method something like this :
add: function(models, options) {
var modelClass = this.model;
isProperIns = this.models.every.(function(model){
return model instanceof modelClass;
});
if (!isProperIns) {
throw new Error("Some of models has unacceptable type")
}
return this.set(models, _.extend({merge: false}, options, addOptions));
}
The purpose of a Collection's model property is not to limit which models the Collection can accept. Rather, that property defines the Model class which the Collection will use when it needs to create a new Model. For instance,when you pass an object literal of Model attributes (as opposed to an instantiated Model) to JokesCollection.add, or when you fetch models in to a JokesCollection, Backbone will use Joke as the Model to instantiate those new additions to the Collection.
There are two ways to ensure your JokesCollection is only populated with instances of Joke. The first way is to never add Model instances to the JokesCollection directly, and instead either:
A) Bring new Jokes in from the server by calling fetch on a JokesCollection
B) add only "raw" Model attributes to the JokesCollection; don't add instantiated Models
However, if you're concerned about a developer accidentally adding a non-Joke Model to the Collection, your other option (as first suggested by #Evgeniy) is to overwrite your JokesCollection's add method. Unlike #Evgeniy's answer though I would not recommend re-writing Backbone's internals. Instead, I would use a simple overwrite that just calls the base Backbone method if possible:
add: function(models, options) {
if (models instanceof Joke) {
// Use the normal Backbone.Collection add method
return Backbone.Collection.prototype.add.call(this, models, options);
}
var allModelsAreJokes = _(models).all(function(model) {
return model instanceof Joke;
));
if (allModelsAreJokes) {
// Use the normal Backbone.Collection add method
return Backbone.Collection.prototype.add.call(this, models, options);
}
// Handle the case where non-Jokes are passed in; either:
// A) convert whatever was passed in to be a Joke:
// var rawModels = _(models).isArray() ? _(models).invoke('toJSON') : model.toJSON();
// return Backbone.Collection.prototype.add.call(this, rawModels, options);
// B) just don't add anything
}

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.

Backbone / Marionette ItemView strategy

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

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)...

How can I pull any class by its cid in Backbone?

In using Backbone.js, I've noticed that both views and models are given cids. I understand that if these classes are part of a collection, I can pull any of them by collection.getByCid. Is it at all possible to pull any class, outside of a collection, given its cid, using Backbone?
For example, if I have MyObject.Views.Tree = Backbone.View.extend({ });, I can create a new Tree view from var tree = new MyObject.Views.Tree();. Calling tree.cid returns a specific cid--something like view231. Is there any way to reference my tree view given only its cid? A global Backbone.getByCid method, perhaps?
ExtJS spoiled me and I felt the need to recreate something similar for Backbone. Maybe this will help you out too? I haven't tested it too much, but it's a very simple change. Just be careful of creating lots of things and not removing them, or you'll have a bunch of registered objects eating up memory.
Backbone.View.Registry = {
items: {},
register: function (id, object) {
this.items[id] = object;
},
lookup: function (id) {
return this.items[id] || null;
},
remove: function (id) {
delete this.items[id];
}
}
Backbone.RegisteredView = Backbone.View.extend({
initialize: function () {
Backbone.View.prototype.initialize.apply(this);
this.cid = this.options.id || this.cid; //just in case you want to assign a unique ID and lookup from that.
Backbone.View.Registry.register(this.cid, this);
},
remove: function () {
Backbone.View.prototype.remove.apply(this);
Backbone.View.Registry.remove(this.cid);
return this;
}
});
test = Backbone.RegisteredView.extend({
intialize: function () {
return $("<div></div>"); //Just return something for this example
}
});
div1 = new test({id: 'header_pane'});
div2 = new test();
console.log(Backbone.View.Registry.items); //Will have the header_pane and a cid based obj in it
ref = Backbone.View.Registry.lookup('header_pane');
console.log(ref); //Will be the header_pane object
div1.remove();
console.log(Backbone.View.Registry.items); //Will only have the cid based object in it
No.
I think you have a slight misunderstanding of the backbone programming model, as well as JavaScript in general. Backbone doesn't keep track of what you create; it only helps you create objects with specific prototypes (Models, Collections, etc.). It doesn't care at all what you do with them. The CID is just a convenience method you can use for indexing and cross-referencing, but you have to write the indices and cross-references yourself.
So if you create an object and don't keep a reference to it somewhere (in a collection, in your router, in another object), it becomes inaccessible and the JavaScript VM will eventually garbage collect it.

Resources