Backbone.js - Given an element, how do I get the view? - javascript

I've created a bunch of Backbone.js views. Each view has an associated element (view.el).
Given an element on the page — out of context of the view — what would be the best way to get the view for the element?
For example, say some event affects a bunch of elements on a page and I want to call a method on every view associated with the affected elements.
One way would be to assign the view to the element's data, but I'm wondering if I've missed something smarter:
var myview = BackBone.View.extend({
initialize: function(options) {
$(this.el).data('view', this);
...
}
});
(I'm using Backbone with jQuery 1.5.)

I've just written a jQuery plugin for this. It also uses the .data() method.
Registration:
I have wrapped / proxied the Backbone View setElement method to attach the required data to the view's $el property.
Registration is done behind the scenes like so:
$(myViewsEl).backboneView(myView);
Retrieval:
The plugin traverses up the DOM hierarchy (using .closest()) until it finds an element with the required data entry, i.e a DOM element with an associated view:
var nearestView = $(e.target).backboneView();
In addition, we can specify what type of Backbone View we wish to obtain, continuing up the hierarchy until we find an instance of matching type:
var nearestButtonView = $(e.target).backboneView(ButtonView);
JSFiddle Example:
Can be found here.
Notes:
I hope I am correct in thinking there are no memory leaks involved here; An 'unlink' is performed if setElement is called a second time round, and since removing a view's element calls .remove() by default, which destroys all data as well. Let me know if you think differently.
The plugin code:
(function($) {
// Proxy the original Backbone.View setElement method:
// See: http://backbonejs.org/#View-setElement
var backboneSetElementOriginal = Backbone.View.prototype.setElement;
Backbone.View.prototype.setElement = function(element) {
if (this.el != element) {
$(this.el).backboneView('unlink');
}
$(element).backboneView(this);
return backboneSetElementOriginal.apply(this, arguments);
};
// Create a custom selector to search for the presence of a 'backboneView' data entry:
// This avoids a dependency on a data selector plugin...
$.expr[':'].backboneView = function(element, intStackIndex, arrProperties, arrNodeStack) {
return $(element).data('backboneView') !== undefined;
};
// Plugin internal functions:
var registerViewToElement = function($el, view) {
$el.data('backboneView', view);
};
var getClosestViewFromElement = function($el, viewType) {
var ret = null;
viewType = viewType || Backbone.View;
while ($el.length) {
$el = $el.closest(':backboneView');
ret = $el.length ? $el.data('backboneView') : null;
if (ret instanceof viewType) {
break;
}
else {
$el = $el.parent();
}
}
return ret;
};
// Extra methods:
var methods = {
unlink: function($el) {
$el.removeData('backboneView');
}
};
// Plugin:
$.fn.backboneView = function() {
var ret = this;
var args = Array.prototype.slice.call(arguments, 0);
if ($.isFunction(methods[args[0]])) {
methods[args[0]](this);
}
else if (args[0] && args[0] instanceof Backbone.View) {
registerViewToElement(this.first(), args[0]);
}
else {
ret = getClosestViewFromElement(this.first(), args[0]);
}
return ret;
}
})(jQuery);

Every view can register for DOM events. As such, every view with the kind of element that you are interested in should register for the DOM event and then assign an event-responding function that does what you want. If you need to DRY things up, use mixin techniques to mix in the function.
I think maybe this solution is easier than you may have initially imagined. Just let the views do the work that they are intended to do.

You could maintain a views hash (dictionary) that uses the element as the key and returns the view (or views).
http://www.timdown.co.uk/jshashtable/

I've been using a method inspired by Ed's solution but it does not require the use of jQuery. It does two things:
It sets the attribute data-backbone-view on the root elements of all views. This is convenient when looking at the DOM tree in a debugger. You can immediately see which elements are associated with views. You can also use the CSS selector [data-backbone-view] to find elements that are the roots of Backbone views.
It adds a backboneView property to each root element of a view. It is then possible to get from the DOM element to the view by looking a the DOM element's properties.
I turn this on only when I'm debugging. Here is the code:
var originalSetElement = Bb.View.prototype.setElement;
Bb.View.prototype.setElement = function setElement(element) {
if (this.el && this.el !== element) {
delete this.el.backboneView;
}
element.backboneView = this;
element.setAttribute("data-backbone-view", "true");
return originalSetElement.apply(this, arguments);
};

Since every view has a reference to the model its displaying, what I would do is assign id of the model to the view's associated element(hopefuly that is not affected by the changes by outside event). Also make sure that the model has a reference to its view. Then have these models stored in a Backbone collection.
With this setup, once something happens to an element, you use the elements id to retrieve corresponding model from Backbone collection that you created above and then this model will give you your view reference.

Related

Backbone : listen to add/remove event of a collection inside model in a collection.

I have a collection x (one instance) of models y.
var y = Backbone.Model.extend({
defaults: {
name: null,
otherCollection: null
}
});
otherCollection (multiple instances) is a collection of models z.
Which event do I listen to inside collection x so that I would get notified when any z is added in / removed from otherCollection?
Backbone by default only support event propagation from Contained Model --> Containing Collection. It won't do the sort of deep nested event propagation you're looking for without some extra effort.
To do it from Contained Collection --> Containing Model, you could do something like the following. Here we're setting up an EpisodeCollection, which will be contained in a SeriesModel. Whenever changes happen to the episode collection changes, we want to hear about it in the series:
var EpisodeCollection = Backbone.Collection.extend({
comparator: 'id'
// ... whatever you want
});
var SeriesModel = Backbone.Model.extend({
initialize: function() {
if (this.has('episodes')) {
this.listenTo(this.get('episodes'), 'all', this._onEpisodeChange);
}
},
_onEpisodeChange: function() {
var eventArgs = Array.prototype.slice.call(arguments, 1);
var eventName = arguments[0];
var toTrigger = 'change:episodes:' + eventName + ' change:episodes change';
this.trigger(toTrigger, eventArgs);
}
});
Here's a running JSFiddle illustrating this with a glorious Star Wars example:
http://jsfiddle.net/9kn6uqdn/4/
You can imagine building similar connections for deeper nesting as needed.
A great thing about Backbone is that it isn't an opinionated framework; it leaves the opinions to the application implementors (us!). To that end, I have tried deeply nested event propagation on a few different occasions, and each time have ended up going an alternative direction. Even in the simple example above, you can see that modifying the title of Episode 6 actually fires 6 different events (which likely isn't what you expect).
You can improve this with more effort, but it can get startlingly complex very quickly. Event propagation in large apps can be hard to reason about, so in my mind the simpler the event flow the easier it is to manage & maintain.
var ModelY = Backbone.Model.extend({});
var ModelZ = Backbone.Model.extend({});
var CollectionX = Backbone.Collection.extend({
model : ModelY,
initialize:function(){
this.listenTo(otherCollection,'add',function(){
alert('model added to otherCollection');
});
}
});
var OtherCollection = Backbone.Collection.extend({ model : ModelZ });
var otherCollection = new OtherCollection();
var collectionX = new CollectionX();
otherCollection.add(new ModelZ());
http://jsfiddle.net/eedfhm7f/
References : http://backbonejs.org/#Events-listenTo

find class instance by property in javascript

I'd like to retrieve an instance of some js Class with only the value of a parameter
lets say :
function myClass(id)
{
this.id = id
}
myClass.prototype.sayHello = function()
{
alert('hello');
}
myClass.instanceFromID = function()
{
...
}
var instance1 = new myClass(22);
var instance2 = new myClass(33);
var instance3 = new myClass(232);
var instance4 = new myClass(1533);
I would love to be able to access the instance by some method like
myClass.instanceFromID(33).sayHello();
I've been thinking of 2 solutions but they seam overkill :
First idea:
storing all the instances in an array, (global scope or static to the class) and iterating over all of them to find and return the instance, but this implies to keep track, add and remove the instances from the array.
Second idea:
make each instance listen to a custom event triggered from the document and compare the data emitted with inner parameter to check if it is concerned and emit a signal back to the document.
How can I achieve this in a simple way or is there no other way than these 2?
Based on what you've written, having the class itself keep track of instances with an instance variable seems to be the approach you're looking for. Of course, this means that instances will never be garbage collected unless you explicitly release them. But perhaps that isn't an issue for you. Here's how I would tackle this:
function MyClass(id) {
this.id = id;
MyClass.instances[id] = this;
}
MyClass.instances = {};
MyClass.instanceFromId = function(id) {
return MyClass.instances[id];
}

Backbone subview not rendered properly

I started developping a website using backbone.js and after trying during the whole morning, i'm quite stuck on the following problem.
I output here only the relevant code.
I've a View called Navigator, that contains a Collection of Records (initially empty) :
var NavigatorView = Backbone.View.extend({
template: JST['app/scripts/templates/Navigator.ejs'],
tagName: 'div',
id: '',
className: 'saiNavigator',
events: {},
initialize: function () {
this.currentRecords = new RecordsCollection();
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this));
},
onRecordsCollectionReseted: function(){
this.render();
},
render: function () {
var tplResult = this.template({
computeTemplate: this.computeTemplate,
records: this.currentRecords
});
this.$el.html(tplResult);
},
onDOMUpdated: function(){
var me = this;
var data = {
device : 'web',
gridId : this.model.get('gridId'),
filterId : this.model.get('filterId')
};
$.ajax({
url: App.getTokenedUrl() + '/task/getGridData.'+this.model.get('taskId')+'.action',
success: me.onRecordReceived.bind(me),
statusCode: {
500: App.handleInternalError
},
type: 'GET',
crossDomain: true,
data : data,
dataType: 'json'
});
},
onRecordReceived: function(result){
var newRecords = [];
for(var i = 0; i < result.items.length; i++){
var newRecord = new RecordModel(result.items[i]);
newRecords.push(newRecord);
}
this.currentRecords.reset(newRecords);
}
});
I've a View called dossier which html is
<div id="dossier1" class="dossier">
<div id="dossier1-navContainer" class="navigatorContainer"/>
<div class="pagesNavigatorContainer"/>
<div class="pagesContainer"/>
<div class="readOnlyFiche"/>
</div>
When i first render the dossier (and i render it only once) i create the navigator in the following render function
render: function () {
this.$el.html(this.template({
uniqBaseId: this.id,
className: this.className
}));
var nav = this.navigator = new NavigatorView({
model : this.model,
id: this.id+'navigator',
el: $('#'+this.id+'-navContainer')
});
this.navigator.render();
//We notify the navigator that it's ready. This will allow the nav to load records
nav.onDOMUpdated();
}
}
As we can see, i give the '#dossier1-navContainer' id to the navigator so that he renders there
So, here is how it works. When i render the dossier, it creates a navigator and inserts it in the DOM. When done, i notify the navigator that it can load its data from the server trough ajax request. When i receive the answer i reset the collection of data with the incoming record.
Juste before the this.$el.html(tplResult) in the navigator render function i output the resulting string.
First time it's
<div class="items"></div>
Second time when i get records, it's
<div class="items">
<div>item1</div>
<div>item2</div>
<div>item3</div>
</div>
So the template generation is correct. However, when the second rendering occurs, the this.$el.html(tplResult) does NOTHING. If i look at the DOM in the browser NOTHING CHANGED
However if i replace this line by
$('#dossier1-navigator').html(tplResult)
it works. Which means that the first time, $('#dossier1-navigator') and this.$el are the same object, the second time not.
I've NO idea why it doesn't work the second time with the standard this.$el.
Help!!
Thanks in advance
Edit : after discussing a lot with Seebiscuit, i'm adding the few lines that helped answering the question
newTask.render();
var taskHtml = newTask.$el.html();
$('#mainTaskContainer').append(taskHtml);
My hunch is that your having a binding problem. I would suggest that you replace
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this)); },
in your initialize, with:
this.listenTo(this.currentRecords, "reset", this.render);
No need to specially bind. Backbone's listenTo bids the callback to the Backbone object that sets the listener (the this in this.listenTo). Also has the added benefit that when you close the view (by calling this.remove()) it'll remove the listener, and help you avoid zombie views.
Try it out.
I think the problem is that you are not using what your are passing to your navigatorView;
In your navigatorView try this:
initialize:function(el) {
this.$el=el
...
}
Let me know if it helps
After countless minutes of discussion with seebiscuit, we came up with the solution. The problem is all on the definition of the $el element. The formal definition defines it as
A cached jQuery object for the view's element. A handy reference instead of re-wrapping the DOM element all the time
This is actually not very exact from a standard cache point of view. From my point of view at least the principle of a cache is to look for the value if it doesn't have it, and use it otherwise. However in this case this is NOT the case. As Seebiscuit told me,
Because when you first bound this.$el = $(someelement) this.$el will always refer to the return of $(someelement) and not to $(someelement). When does the difference matter?
When the element is not in the DOM when you do the assignment
So actually, $el holds the result of the first lookup of the selector. Thus, if the first lookup misses then it won't succeed ever! Even if the element is added later.
My mistake here is to add the main dossierView into the DOM after rendering its NavigatorView subview. I could have found the solution if the $el was a real cache as the 2nd rendering in the ajax callback would have found the element. With the current way $el works i had just nothing.
Conclusion : make sure every part of your view is properly rendered in the DOM at the moment your try to render a subview.

Extending Ember LinkView

Ember.LinkView, the the view class behind the handlebars {{linkTo}} helper is now public in Ember 1.0 RC2. I want to extend it so I can create a custom view without having an extra nested tag for linkTo.
For example:
App.MyLinkView = Ember.LinkView.extend({
namedRoute: 'another'
});
then
{{#view App.MyLinkView}}LinkView to another route{{/view}}
Looked through the source a bit without much luck, as it constantly throws an error.
Here's a jsfiddle with the standard {{linkTo}} working, and the LinkView attempt commented out so it doesn't throw an error.
http://jsfiddle.net/HgmEy/1/
Edit:
Here is a more realistic example of why you would want to do this:
http://jsfiddle.net/HgmEy/3/
The desired functionality is working here using a regular view, however using a LinkView would be preferred to avoid the extra dom element.
LinkView is intended to be created via a helper, which passes (and provides default values for) some options.
Your error occurs when trying to determine whether your custom class is active or not. You'll need to do one of the following
pass or supply the expected default options when using your App.MyLinkView
override the active function and implement what you need
just pass options to {{linkTo}} for the behavior you want
reopen Ember.LinkView to provide the app-wide behavior you'd want
I needed to do this to override Ember.LinkView's call to transitionTo in order to come up with a solution for jQuery animations between transitions. It seems to me that there are a couple of viable ways to override LinkView. The second one I succeeded with is Trek's last option, and is simpler. This is method #2:
Method #2
{{#linkTo 'items' this eventName="myEvent"}} Link to {{title}} {{/linkTo}}
Now rewrite the app-wide LinkView:
Ember.LinkView.reopen({
// this handler is still called on click, but
// if we specify eventName in our template,
// we can call that handler only when we need to,
// or not at all
click: function (e) {
var evtName = this.get('eventName');
// transitionTo was already invoked by
// this._invoke() if evtName was `click`
if (evtName === 'click') return;
e.preventDefault();
// do some stuff here
var args = [].slice.call(arguments);
this.trigger.apply(this, [evtName].concat(args));
}
});
Method #1
The first method I came up with was to extend Ember.LinkView and create a custom Handlebars helper. The Ember source was really handy here for reading, but I had to override a private method, so I don't think this is really ideal. Here's the implementation. Keep in mind I was trying to control when the View triggered a transitionTo:
{{#appLinkTo 'items' this}} Link to {{title}} {{/appLinkTo}}
Now code it up!
App.LinkView = Ember.LinkView.extend({
// always called after this.invoke(),
// which calls transitionTo
click: function (e) {
e.preventDefault();
},
// already bound to the click event by this.init().
// our click handler above always gets called after this one
_invoke: function (event) {
// we need to simulate the old _invoke if we
// want to override its call to transitionTo
//
// https://github.com/emberjs/ember.js/blob/v1.0.0/packages/ember-routing/lib/helpers/link_to.js#L297
var isSimpleClick = Ember.ViewUtils.isSimpleClick;
if (!isSimpleClick(event)) { return true; }
event.preventDefault();
if (this.bubbles === false) { event.stopPropagation(); }
if (this.get('_isDisabled')) { return false; }
if (this.get('loading')) {
Ember.Logger.warn("This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid.");
return false;
}
// now we can start messing around
var routeArgs = this.get('routeArgs');
// routeArgs seems to have format ['routeName', models for dynamic segments]
this.set('routeArgs', ['group', routeArgs[1]]);
// if we use:
this.get('controller').send('someAction', routeArgs);
// the controller can do in its `someAction` handler:
// `this.transitionToRoute.apply(this, routeArgs);`
}
});
// besides the naming, this is verbatim from the end of:
// https://github.com/emberjs/ember.js/blob/v1.0.0/packages/ember-routing/lib/helpers/link_to.js
Ember.Handlebars.registerHelper('app-link-to', function(name) {
var options = [].slice.call(arguments, -1)[0],
params = [].slice.call(arguments, 0, -1),
hash = options.hash;
hash.disabledBinding = hash.disabledWhen;
hash.parameters = {
context: this,
options: options,
params: params
};
return Ember.Handlebars.helpers.view.call(this, App.LinkView, options);
});
Ember.Handlebars.registerHelper('appLinkTo', Ember.Handlebars.helpers['app-link-to']);
Method #3
If you want the best of both, you could combine both methods and extend Ember.LinkView, create a custom Handlebars helper, and use custom event names to signify which actions you want to take. That way, overriding Ember.LinkView, and overwriting _invoke aren't necessary.
Good luck!

Can I call a method on other objects created by the same constructor in javascript?

Lets say I have an object constructor that a third-party page developer can insert in their page, called "Widget". It could be something like a tooltip, for example.
var Widget = function(settings, callbacks) {
this.hide = function() {
//some code...
};
this.show = function() {
this.isShowing = true;
//more code...
}
}
When this Widget is shown, I want to hide any other shown Widget. Is there a way the "show" method can call the "hide" method on the other instance(s) of Widget whose "this.isShowing" is true, or on all instances of Widget?
You can, but you need to maintain references to those instances. You could maintain them on a property of the function itself:
var Widget = function(settings, callbacks) {
Widget.instances.push(this);
// More code
};
Widget.instances = [];
And then, just access Widget.instances to get the all the widgets created.
You may want to create some kind of destructor to remove a widget from the list though (myWidget.destroy() or something), because this might generate a memory leak if you instantiate too many of them.

Categories