In Backbone, how do I have an after_render() on all views? - javascript

I am maintaining a javascript application and I would like there to be a jquery function invoked on pretty much every view. It would go something like this:
SomeView = Backbone.Marionette.ItemView.extend
initialize: ->
#on( 'render', #after_render )
after_render: ->
this.$el.fadeOut().fadeIn()
Clearly there is a better way to do this than have an after_render() in each view? What is the better way to do it? If you can give an answer that includes jasmine tests, I'll <3 you ;)

The event you are looking for is onDomRefresh. See here for the documentation:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#view-domrefresh--ondomrefresh-event

Create your own base view class and put your afterRender code in it. When you create a view, inherit from this class.
var MyApp.ItemView = Backbone.Marionette.ItemView.extend({
afterRender: function() {
// This will be called after rendering every inheriting view.
}
});
var SpecificItemView = MyApp.ItemView.extend({
// this view will automatically inherit the afterRender code.
});
In general, it seems to be considered good practice to define your own base views for all 3 view types. It will enable you to easily add global functionality later.

There is a common pattern used across all Backbone frameworks, normally they have a render method which in turn calls beforeRender, renderTemplate and afterRender methods.
render:function(){
this.beforeRender();
this.renderTemplate();// method names are just indicative
this.afterRender();
return this;
}
In your Base view you can have these methods to be empty functions, and implement them wherever you want it. Not sure this answer applies to Marionette

Combining thibaut's and Robert Levy's answer, the correct solution would be:
var baseView = Backbone.Marionette.ItemView.extend({
onDomRefresh: function() {
// This will be triggered after the view has been rendered, has been shown in the DOM via a Marionette.Region, and has been re-rendered
// if you want to manipulate the dom element of the view, access it via this.$el or this.$('#some-child-selector')
}
});
var SpecificItemView = baseView.extend({
// this view will automatically inherit the onDomRefresh code.
});

Related

MV* in Polymer, models and services as polymer-elements?

Say I want two views (polymer-elements) to share a model for example.
In Angular the model would live in a singleton service that gets injected into the views, both views read from the same source.
I tried emulating this approach with Polymer so I can do something like:
<polymer-element name="view1">
<template>
<my-model></my-model>
...
</template>
...
</polymer-element>
<polymer-element name="view2">
<template>
<my-model></my-model>
...
</template>
...
</polymer-element>
I like this approach because it's a declarative way of defining dependencies, and it basically works the same as <core-ajax> and other "out of the box" Polymer elements.
With this way I need to wait for the domReady lifecycle callback before I can interface with any element declared in the template, so this is where I'm holding my initialisation logic at the minute. The problem is that this callback gets called once for each <my-model> element declared (so <my-model> would be initialised twice in this example because it's present both in <view1> and <view2>). To make sure that my model follows the singleton pattern I have to move state outside of the element instance, something like this:
<polymer-element name="my-model">
<script>
(function(){
// private shared state
var instances = [], registered; // pattern variables
var foo; // state, model, whatever
// element init logic
Polymer('my-model', {
// Polymer callbacks
domReady: function(){
if (registered === (registered=true)) return;
// singleton init logic
foo = 'something';
// event handlers
this.addEventListener('foo', function(){ foo += 'baz'; });
},
attached: function() { instances.push(this); },
detached: function(){
instances = instances.filter(function(instance){
return instance !== this;
}.bind(this));
},
// element API
update: doSomething,
get state() { return foo; }
});
// private functions
function doSomething(){ foo += 'bar' }
})();
</script>
</polymer-element>
So it works but it looks wrong to me. Is using <polymer-element> generally incompatible with the singleton pattern? Should I move away from Polymer for models and services? How do Polymer core-elements get away with it?
[EDIT] I added some event listeners to the initialising code above. They're only registered in one instance to avoid the listeners triggering multiple times across multiple instances. What would happen if the instance where the event handlers are declared gets removed? Will that not break the asynchronous logic?
I'd go like this:
Define your model on the main page and call it from your views.
if it gets removed you could:
1 - listen for the "detached" lifecycle callback and inside it register it imperatively or
2 - store stuff on a prototype build in a higher level object and access it the way you fancy the most.
3 - if all fails, (i'm not sure it will but i guess so as i've yet to use this kind of implementation, as of now i talk to php and pass around objects i need persistent) you could use a "prepareForRemoval" knowing you will leave the instance, local storage your stuff and do number 1 then "recoverFromRemoval" if you know what i mean by camel casing prototype suggestions.
Anyways i'm not very fond of singletons. Polymer is powerful front-end stuff but i'm not sure it's the best way to go about it.
in the API docs they do not mention the possibility of getting it cut off (as you can see)
but i honestly think you're right and you would lose your stuff.
That's just my 2 cents actually just a inellegant sollution i came up for at this very moment, maybe #ebidel, #DocDude or #dodson can help us in that matter but you can't really tag em here on SO i'll tag em on G+ for us, you sir got me intrigued.
BTW why would you move away from your main page? there's no point for it in polymer you should change the content dynamically not get away from it. what would be the usage scenario?
ps.: sorry, i hate capitalizing proper nouns.Get over it
EDIT (wouldn't fit on the comments):
I expressed myself wrong. Anyways i strongly think i wasn't understanding what you wanted.
Well, if i got it right this time yes it will fire multiple times (they are supposed to), but it shouldn't cut others out once a particular view gets removed.
As for your initialisation logic i would go about adding a listener to the window or document (i think window is more advisable) itself waiting for the 'polymer-ready' event.
"To make sure that my model follows the singleton pattern I have to
move state outside of the element instance"
Yes thats right. but don't wait for the domready in it's prototype, instead use a construct or contruct-like and call it it as the callback of the aforementioned event listener. i'll edit my answer to make it clearer (if it's not, let me know) when i get back home. i hope you got i meant.
if you don't i'll be back soon.
In browsers, window == singleton object by definition.
Simple use:
var window.instances = [];
var window.registered;
var window.foo;
instead.
Another way is to use Polymer core-meta element:
<core-meta id="x-foo" label="foo"></core-meta>
<core-meta id="x-bar" label="bar"></core-meta>
<core-meta id="x-zot" label="zot"></core-meta>
<core-meta id="apple" label="apple" type="fruit"></core-meta>
<core-meta id="orange" label="orange" type="fruit"></core-meta>
<core-meta id="grape" label="grape" type="fruit"></core-meta>
<h2>meta-data</h2>
<template id="default" repeat="{{metadata}}">
<div>{{label}}</div>
</template>
<h2>meta-data (type: fruit)</h2>
<template id="fruit" repeat="{{metadata}}">
<div>{{label}}</div>
</template>
<script>
document.addEventListener('polymer-ready', function() {
var meta = document.createElement('core-meta');
document.querySelector('template#default').model = {
metadata: meta.list
};
var fruitMeta = document.createElement('core-meta');
fruitMeta.type = 'fruit';
document.querySelector('template#fruit').model = {
metadata: fruitMeta.list
};
});
</script>

backbone.js - how to communicate between views?

I have a single page web app with multiple backbone.js views. The views must sometimes communicate with each other. Two examples:
When there are two ways views presenting a collection in different ways simultaneously and a click on an item in one view must be relayed to the other view.
When a user transitions to the next stage of the process and the first view passes data to the second.
To decouple the views as much as possible I currently use custom events to pass the data ($(document).trigger('customEvent', data)). Is there a better way to do this?
One widely used technique is extending the Backbone.Events -object to create your personal global events aggregator.
var vent = {}; // or App.vent depending how you want to do this
_.extend(vent, Backbone.Events);
Depending if you're using requirejs or something else, you might want to separate this into its own module or make it an attribute of your Application object. Now you can trigger and listen to events anywhere in your app.
// View1
vent.trigger('some_event', data1, data2, data3, ...);
// View2
vent.on('some_event', this.reaction_to_some_event);
This also allows you to use the event aggregator to communicate between models, collections, the router etc. Here is Martin Fowler's concept for the event aggregator (not in javascript). And here is a more backboney implementation and reflection on the subject more in the vein of Backbone.Marionette, but most of it is applicable to vanilla Backbone.
Hope this helped!
I agree with #jakee at first part
var vent = {};
_.extend(vent, Backbone.Events);
however, listening a global event with "on" may cause a memory leak and zombie view problem and that also causes multiple action handler calls etc.
Instead of "on", you should use "listenTo" in your view
this.listenTo(vent, "someEvent", yourHandlerFunction);
thus, when you remove your view by view.remove(), this handler will be also removed, because handler is bound to your view.
When triggering your global event, just use
vent.trigger("someEvent",parameters);
jakee's answer suggests a fine approach that I myself have used, but there is another interesting way, and that is to inject a reference to an object into each view instance, with the injected object in turn containing references to as many views as you want to aggregate.
In essence the view-aggregator is a sort of "App" object, and things beside views could be attached, e.g. collections. It does involve extending the view(s) and so might not be to everybody's taste, but on the other hand the extending serves as a simple example for doing so.
I used the code at http://arturadib.com/hello-backbonejs/docs/1.html as the basis for my ListView and then I got the following to work:
define(
['./listView'],
function (ListView) {
var APP = {
VIEWS : {}
}
ListView.instantiator = ListView.extend({
initialize : function() {
this.app = APP;
ListView.prototype.initialize.apply(this, arguments);
}
});
APP.VIEWS.ListView = new ListView.instantiator();
console.log(APP.VIEWS.ListView.app);
}
);

Backbone.js: Should .render() and .remove() be able to reverse each other?

Because Backbone.js is pretty flexible, I am wondering about the best approach for certain things. Here I'm wondering if I'm supposed to build my application's views so that '.render()' and '.remove()' correctly reverse each other.
At first, the way that seems cleanest is to pass the view a ID or jQuery element to attach to. If things are done this way though, calling '.render()' will not correctly replace the view in the DOM, since the main element is never put back in the DOM:
App.ChromeView = Backbone.View.extend({
render: function() {
// Instantiate some "sub" views to handle the responsibilities of
// their respective elements.
this.sidebar = new App.SidebarView({ el: this.$(".sidebar") });
this.menu = new App.NavigationView({ el: this.$("nav") });
}
});
$(function() {
App.chrome = new App.ChromeView({ el: $("#chrome") });
});
It seems preferable to me to set it up so that .remove() and .render() are exact opposites:
App.ChromeView = Backbone.View.extend({
render: function() {
this.$el.appendTo('body');
this.sidebar = new App.SidebarView({ el: this.$(".sidebar") });
this.menu = new App.NavigationView({ el: this.$("nav") });
}
});
$(function() {
App.chrome = new App.ChromeView();
});
What does the Backbone.js community say? Should .remove() and .render() be opposite sides of the same coin?
I prefer that render does NOT attach the view's element to the dom. I think this promotes loose coupling, high cohesion, view re-use, and facilitates unit testing. I leave attaching the rendered element to a container up to either the router or a main "layout" type container view.
The nice thing about remove is that it works without the view having knowledge of the parent element, and thus is still loosely coupled and reusable. I definitely don't like to put random DOM selectors from my layout HTML (#main or whatever) into my views. Definitely bad coupling there.
I will note that in certain annoying situations, some things like the chosen jQuery plugin require some code to run AFTER the element has been attached to the DOM. For these cases I usually implement a postAttach() callback in the view and try to keep the amount of code there as small as possible.
Yes, the in-house View.remove() is very agressive.
For the propose of re-create the View again using an external el I am used to rewrite it like this:
remove: function(){
this.$el.empty();
return this;
}
But I don't think the framework should implement magic behavior to avoid this external DOM elements deletion.
This framework behavior is aggressive, ok, but it is very cheap to customize it when needed as we see above.
What about this? If we just have .initialize and .render take a parentSelector property, we can do this and end up with a usage that is:
Loosely coupled
Reversable .remove()/.render()
Single method
instantiation & rendering for the calling method
eg:
// Bootstrap file
curl(['views/app']).then(
function(App){
app = new App('body');
});
// view/app.js
define([
'text!views/app.tmpl.html'
, 'link!views/app.css'
]
, function (template) {
var App
// Set up the Application View
App = Backbone.View.extend({
// Standard Backbone Methods
initialize: function (parentSel) {
console.log('App view initialized')
if (parentSel) { this.render(parentSel) }
}
, render: function (parentSel) {
if (parentSel) { this._sel = parentSel } // change the selector if render is called with one
if (!this._sel) { return this } // exit if no selector is set
this.$el.appendTo(this._sel)
this.$el.html(
this.compiledTemplate({ 'content':'test content' })
);
return this
}
// Custom Properties
, compiledTemplate: _.template(template)
})
return App
});
// External usage
// I can call .remove and .render all day long now:
app.remove()
app.render()
app.remove()

How to specify a method callback when a Backbone View is inserted into the DOM?

I need to run a layout script as soon as my views are inserted into the DOM. So...
$(".widgets").append(widgets.render().el)
$(".widgets .dashboard").isotope # <-- This needs to be called whenever new widgets are inserted
The problem is I have to insert new widgets a few different views and re-call this script a few different places, which is not DRY. I am wondering how I can define the isotope in the View class.
Would it be a good idea to define an event listener to watch for append into the ".widgets" and to run the script? Is there a built in way of building views that are smart about when they are added to the DOM?
(For that matter, it would be also useful to define a callback for when a View is removed from the DOM.)
How about calling the isotope each time the view is rendered? You'll need to be careful to call render() only after the widget is injected, but this ought to take care of your problem:
//in Backbone.view.extend({
initialize: function() {
// fix context for `this`
_.bindAll(this);
},
render: function() {
// .. do rendering..
this.isotope();
return this;
}
// }) // end .extend
use:
var self = this;
this.$el.on('DOMNodeInserted', function(evt){
self.isotope();
$(evt.target ).stopPropagation();
})

When initializing a backbone view

How do I make it so that a function runs every time a backbone.js view is initialized?
I'm looking for something that I can put on outside of my normal view code, as an extension to backbone.js.
The idea is to reduce the amount of boilerplate.
Since Javascript is not a true object oriented programing language, you can't use inheritance to solve your problem as you could if it was java or c#.
One possible solution is to use the factory design pattern.
Instead of instantiating your view directly, you can call a factory method that will instantiate your view.
var viewFactory = function(view, viewOptions) {
//perform your boilerplate code
return new view(viewOptions);
}
AView = Backbone.View.extend({});
var person = new Backbone.Model({name: 'Paul'});
var view = viewFactory(AView, { model: person });
Here's a jsfiddle example
It's not as an elegant solution that is possible with other languages, but it does the job.
use the builtin backbone.js initialize function:
http://documentcloud.github.com/backbone/#View-constructor
var ItemView = Backbone.View.extend({
initialize: function(){
alert('View Initialized');
}
});
EDIT: I should be more clear.
In the words of Patrick Ewing found here http://podcast.rubyonrails.org/programs/1/episodes/railsconf-2007:
"if it walks like a duck and talks like a duck, it’s a duck, right? So if this duck is not giving you the noise that you want, you’ve got to just punch that duck until it returns what you expect"
Duck Punch (or Monkey Patch if you prefer) the Backbone object.
Backbone.View.prototype.initialize = function(){
alert('I overrode the default initialize function!');
}
You can use Backbone.Events.
On the top level of your app or on the global object:
app.eventManager = {};
_.extend(app.eventManager, Backbone.Events);
app.eventManager.bind("newView", app.yourfunction(view));
And in the initialize method of any view you want to trigger your function:
app.eventManager.trigger("newView", this);
where "this" is the view instance passed as the "view" parameter to your function.

Categories