Backbone-on-rails event aggregator - backbone.js

I have decoupled views that I want to respond to each other's events. I added an event aggregator in my application.js file and triggered the event from my function in the first view. I can't seem to get the second view to respond to that event, though. I've looked at Derrick Bailey's discussion of event aggregators and the documentcloud.github.io discussion. I haven't been able to find syntax that works with coffeescript and I'm not sure how to translate the syntax I have found.
My first view that I want to publish the event looks like this:
class Mlp.Views.PoniesIndex extends Backbone.View
poniesTemplate: JST['ponies/index']
events:
'submit #new_pony': 'createPony'
'click #pony_up': 'ponyUp'
initialize: ->
console.log(Mlp.Collections.Settings)
#collection.on('reset', #render, this)
#collection.on('add', #appendPony, this)
#collection.on('change', #render, this)
ponyUp: (event) ->
event.preventDefault()
Mlp.vent.trigger('ponyUp', #collection)
#collection.ponyDown()
#collection.ponyUp()
My event aggregator is in the application.js file:
//= require jquery
//= require jquery_ujs
//= require underscore
//= require backbone
//= require mlp
//= require_tree ../templates
//= require_tree ./models
//= require_tree ./collections
//= require_tree ./views
//= require_tree ./routers
//= require_tree .
Mlp.vent = _.extend({}, Backbone.Events);
When I add this to my application.js file I get the console log I want:
Mlp.vent.bind("ponyUp", function(collection){
console.log("pony up!");
});
But when I try to add an event binder in the view I want to receive the published event I get nothing:
class Mlp.Views.SettingsIndex extends Backbone.View
template: JST['settings/index']
events:
'submit #new_setting': 'createSetting'
'click #change_of_venue': 'randomSetting'
initialize: ->
#collection.on('ponyUp', #alterbox, this)
#collection.on('reset', #render, this)
#collection.on('add', #appendSetting, this)
#collection.on('change', #render, this)
alertbox: (collection) ->
event.preventDefault()
console.log("pony up!")
I have tried a variety of diffent syntaxes but i'm still not sure whether I should be binding to the collection in the intialization or binding in the events declarations. The examples I've seen use underscore's _.bindAll. When I tried that it threw "cannot read 'bind' of undefined", even though i'm requiring underscore in my application.js. I'm sure this is some basic syntax that I don't understand.
Thank you in advance for any help!

Your collection won't send out a 'ponyUp' event, your event aggregator (Mlp.vent) will. When you say this:
Mlp.vent.trigger('ponyUp', #collection)
You're asking Mlp.vent to send out a 'ponyUp' event with #collection as the argument, you're not triggering a 'ponyUp' event from #collection. That means that you'd want to listen to Mlp.vent for the event and this:
#collection.on('ponyUp', #alterbox, this)
should look more like this:
Mlp.vent.on('ponyUp', #alterbox, this)
Alternatively, you could trigger the event on the collection by hand with something like this:
#collection.trigger('ponyUp', #collection)
The main difference between the two approaches is that using Mlp.vent allows anyone to listen for the event even if they don't have the collection in hand whereas the #collection.trigger approach requires everyone to have a reference to the collection before they can listen for the event.

Related

Backbone collection.where error

I assume I am doing something silly and clearly wrong, but I am baffled. Using CoffeeScript and marionette backbone, I want to create a methods on my collection that will set most models to selected and then set all selected to unselected. I assumed
deselectAll: ->
#where({selected: true})
would get me the selected models and I could iterate over that (for model in selected) and set the models. But I cannot get #where to work and instead get:
Uncaught TypeError: _.matches is not a function
from backbones:
where: function(attrs, first) {
var matches = _.matches(attrs);
return this[first ? 'find' : 'filter'](function(model) {
return matches(model.attributes);
});
},
UPDATE
It is currently working with filter:
#filter (model) ->
model.get 'selected'
and it seems somewhat more complicated than where was, but perhaps not?
backbone <= 1.2.1 is not compatible with underscore >= 1.8.0. Upgrade your backbone to 1.2.2 or 1.2.3.
In underscore 1.8.0 function matches is deprecated and renamed to matcher. See underscore changelog

Properly closing views in Marionette

I have an application built on Backbone.Marionette with a CollectionView that instantiates many CompositeViews, which render a tree structure.
I've read through Zombie Views (Bailey on Zombies) and through View and Region documentation. However, if everything looks pretty simple when reading, execution is a different issue altogether.
When I hit any of my routes, my keyboard shortcuts end up being fired multiple times. I found a work around, but this workaround causes other problems on rendering changes in the views.
Here is the actual code that triggers the keyboard shortcuts multiple times. In Snippet A, I have added any way of closing the view that I could think of, despite the fact that normally, closing a view should only require App.contentRegion.currentView.treeRegion.close()
showContentView: (tree) ->
if #treeView?
App.contentRegion.currentView.treeRegion.reset()
App.contentRegion.currentView.treeRegion.close()
#treeView.close()
delete #treeView
#treeView = new App.Note.TreeView(collection: tree)
App.contentRegion.currentView.treeRegion.show #treeView
Snippet B, below, fixes the keyboard shortcut issue. However, it causes the issue where additionally created models (CompositeView) aren't rendered to the user.
showContentView: (tree) ->
if #treeView?
#treeView.collection = tree
#treeView.render()
else
#treeView = new App.Note.TreeView(collection: tree)
App.contentRegion.currentView.treeRegion.show #treeView
Here is where I initialize the CollectionView, which in turn renders the CompositeViews
initialize: -> # collectionView
#listenTo #collection, "sort", #render
#listenTo #collection, "destroy", #addDefaultNote
Note.eventManager.on 'createNote', #createNote, this
Note.eventManager.on 'change', #dispatchFunction, this
#drag = undefined
initialize: -> # compositeView
#collection = #model.descendants
#bindKeyboardShortcuts()
#listenTo #collection, "sort", #render
Note.eventManager.on "setCursor:#{#model.get('guid')}", #setCursor, #
Note.eventManager.on "render:#{#model.get('guid')}", #render, #
Note.eventManager.on "setTitle:#{#model.get('guid')}", #setNoteTitle, #
This is how I bind my keyboard shortcuts in the CompositeViews
bindKeyboardShortcuts: ->
#.$el.on 'keydown', null, 'ctrl+shift+backspace', #triggerShortcut 'deleteNote'
#.$el.on 'keydown', null, 'tab', #triggerShortcut 'tabNote'
#.$el.on 'keydown', null, 'shift+tab', #triggerShortcut 'unTabNote'
#.$el.on 'keydown', null, 'alt+right', #triggerShortcut 'tabNote'
#.$el.on 'keydown', null, 'alt+left', #triggerShortcut 'unTabNote'
#.$el.on 'keydown', null, 'alt+up', #triggerShortcut 'jumpPositionUp'
#.$el.on 'keydown', null, 'alt+down', #triggerShortcut 'jumpPositionDown'
#.$el.on 'keydown', null, 'up', #triggerShortcut 'jumpFocusUp'
#.$el.on 'keydown', null, 'down', #triggerShortcut 'jumpFocusDown'
#.$el.on 'keydown', null, 'alt+ctrl+left', #triggerShortcut 'zoomOut'
#.$el.on 'keydown', null, 'alt+ctrl+right', #triggerShortcut 'zoomIn'
And how I trigger them
triggerShortcut: (event) -> (e) =>
e.preventDefault()
e.stopPropagation()
#triggerEvent(event).apply(#, Note.sliceArgs arguments)
triggerEvent: (event) ->
(e) =>
#updateNote()
args = ['change', event, #model].concat(Note.sliceArgs arguments, 0)
Note.eventManager.trigger.apply(Note.eventManager, args)
Finally, to make sure everything is clean, I unbind every shortcut in onBeforeClose. I also unbind any of eventManager's listeners.
onBeforeClose: ->
console.log "view being closed", #
#$el.off()
Note.eventManager.off "setCursor:#{#model.get('guid')}"
Note.eventManager.off "render:#{#model.get('guid')}"
Note.eventManager.off "setTitle:#{#model.get('guid')}"
Note.eventManager.off "timeoutUpdate:#{#model.get('guid')}"
I know the problem comes from #treeView = new App.Note.TreeView(collection: tree). If I create a new TreeView on each #showContentView (Snippet A), every added model is properly rendered to the view, but the shortcuts get crazy.
On the other hand, if I create one TreeView and swap it's collection (Snippet B), I get the rendering problem in the views, but shortcuts are fine!
I tried to include everything you would need, nothing more (it's already quite some code..) but if you guys need anything else, please ask!
Hope I could get that clear enough..
[Edit]
I have tried many different combinations to get rid of the shortcut bug, but if I create a new TreeView on each showContentView, nothing seems to close the view properly.
I think this is coming from a deeper memory leak problem. I will probably write an other StackOverflow quesion in that regard, and link to this one for additional information.
Thank you very much!
I figured out what was the issue here.
Using Snippet A and chrome devtool's profiler, I could track down the leak. The onClose method that I provided in my question comes from the CompositeView, where the keyboard shortcut are bound.
The problem was the CollectionView wasn't getting garbage collected because of the use of Note.eventManager.on, which was keeping a reference to the view.
So I added a onBeforeClose method to the TreeView (CollectionView)
onBeforeClose: ->
Note.eventManager.off('createNote', #createNote, this)
Note.eventManager.off('change', #dispatchFunction, this)
#drag = undefined
With this onBeforeClose, the view is now being properly closed, which in turn allows the children views to be closed as well and to stop listening to the shortcuts being fired.
I guess that was pretty obvious, once I found out, but I wanted to add this answer so it makes clear any event listener that you set up without #listenTo doesn't get cleared up by Marionette and needs to be properly handled.
[edit]
To follow up on the comments, here would have been a better solution from the start :
Replace
initialize: -> # compositeView
/* ... */
Note.eventManager.on "setCursor:#{#model.get('guid')}", #setCursor, #
Note.eventManager.on "render:#{#model.get('guid')}", #render, #
Note.eventManager.on "setTitle:#{#model.get('guid')}", #setNoteTitle, #
And
initialize: -> # collectionView
/* ... */
Note.eventManager.on 'createNote', #createNote, this
Note.eventManager.on 'change', #dispatchFunction, this
with
initialize: -> # compositeView
/* ... */
#listenTo Note.eventManager, "setCursor:#{#model.get('guid')}", #setCursor
#listenTo Note.eventManager, "render:#{#model.get('guid')}", #render
#listenTo Note.eventManager, "setTitle:#{#model.get('guid')}", #setNoteTitle
/* ... */
initialize: -> # collectionView
/* ... */
#listenTo Note.eventManager, 'createNote', #createNote, this
#listenTo Note.eventManager, 'change', #dispatchFunction, this
Using the listenTo syntax as so would have prevented the memory leak in the first place.
As so, the onBeforeClose blocks can be completely removed!

Support for Dot Notation in Backbone Models

How to get support for dot notation/nested objects in backbone model. The plugins that are available are buggy and wondering if backbone would ever support
person = { name : {first: 'hon',last:'son'}}
model = new Backbone.Model(person)
model.get('name.first')
model.set('name.first','bon')
There are two plugins to get the job done:
Backbone Nested
Backbone Deep Model
Both handle getting and setting attributes and change events for dot notation.
I did that if i were you:
var nameObj = model.get('name')
nameObj.first = bon
model.set('name', nameObj)

Best Practice to add UI enhancements to multiple Backbone Marionette views

So I was what the best way for all views in an application to have actions performed on an element.
In a non single page application you would run say:
$(document).ready(function() {
$('.autosize').autosize();
});
to apply autosize function to all elements with the autosize class on every page.
Now in a Backbone Marionette app to do this you could perform that in each view with onDomRefresh or similar but for things that affect 90% of views you'd want this to run automatically somehow.
I don't think there's a way that an Application object can listen to all onDomRefresh events which would potentially solve it. I've consider overloading Marionette.MonitorDOMRefreshto add this in but it doesn't feel like a Backbone approach.
Other things I considered were sub-classing each of the marionette views to add mixins for loading different groups of UI elements.
I figured other people must have experienced this scenario so was interested what approaches have been used.
Just make a base View class and inherit from it every view class that needs the autosize enhancement.
var AutosizeBaseView = Backbone.Marionette.ItemView.extend({
onDomRefresh: function(){
this.$('.autosize').autosize();
}
});
then make your classes like this:
var SomeView = AutosizeBaseView.extend({
});
So I couldn't really find any solutions that really solved my problem, despite some helpful chats with #julio_menedez and #marionettejs on Twitter. With a really good idea being using Polymer but wasn't suitable as I need to support older IE's.
So instead I headed into the dangerous world of monkey patching to solve it (Bear in mind I might need to iron out some wrinkles with this still, just finished writing it and not fully tested it - I'll update accordingly)
In Coffeescript: (javascript version at the bottom)
# Monkey patching the Marionette View.. sorry!
# this is the only Marionette view which doesn't have it's own constructor
Marionette.ItemView = Marionette.ItemView.extend
constructor: ->
Marionette.View.prototype.constructor.apply #, Array.prototype.slice.call(arguments, 0)
original_view_constructor = Marionette.View.prototype.constructor
Marionette.View.EventAggregator = event_aggregator = _.extend {}, Backbone.Events
# all the other constructors call this so we can hijack it
Marionette.View.prototype.constructor = ->
event_aggregator.listenTo #, 'all', =>
args_array = Array.prototype.slice.call arguments, 0
event_aggregator.trigger.apply event_aggregator, [ 'view:' + args_array[0], # ].concat(args_array.slice(1))
event_aggregator.stopListening # if args_array[0] == 'close'
original_view_constructor.apply #, Array.prototype.slice.call(arguments, 0)
And then to use I just setup a listener in my application object to catch view events I need. e.g:
#listenTo Marionette.View.EventAggregator, 'view:dom:refresh', (view) ->
view.$('div').css('backgroundColor', 'red');
So in my view these are the pros and cons of this technique:
Pros:
Can listen to all view events without injecting all view classes or subclassing all view classes
Simple to use
Objects don't need to opt-in to using it at all
Cons
Uses monkey patching, dangerous to Marionette API Changes
Uses Marionette namespacing so vulnerable to a future Marionette namespace collision
Takes dealing with views out of view context
Having an event aggregator object isn't something seen elsewhere in Backbone/Marionette (afaiw) so breaks a pattern (update - something similar is seen with Backbone.history)
Anyway I'm welcome to feedback, alternatives, criticism :-) and hope maybe this helps someone else in the same situation
Javascript:
(function() {
var event_aggregator, original_view_constructor;
Marionette.ItemView = Marionette.ItemView.extend({
constructor: function() {
return Marionette.View.prototype.constructor.apply(this, Array.prototype.slice.call(arguments, 0));
}
});
original_view_constructor = Marionette.View.prototype.constructor;
Marionette.View.EventAggregator = event_aggregator = _.extend({}, Backbone.Events);
Marionette.View.prototype.constructor = function() {
var _this = this;
event_aggregator.listenTo(this, 'all', function() {
var args_array;
args_array = Array.prototype.slice.call(arguments, 0);
event_aggregator.trigger.apply(event_aggregator, ['view:' + args_array[0], _this].concat(args_array.slice(1)));
if (args_array[0] === 'close') {
return event_aggregator.stopListening(_this);
}
});
return original_view_constructor.apply(this, Array.prototype.slice.call(arguments, 0));
};
}).call(this);
In CoffeeScript I think you could also do:
extend = (obj, mixin) ->
obj[name] = method for name, method of mixin
obj
include = (klass, mixin) ->
extend klass.prototype, mixin
include Marionette.View,
onDomRefresh: () -> #$('.autosize').autosize()
Which should cover all the view types. Haven't tested this specifically, but just did something very similar to add functionality to Marionette's Layout view. Extend / include pattern at http://arcturo.github.io/library/coffeescript/03_classes.html. Of course this should all be doable in straight up JS too.
UPDATE:
Actually, since we have Underscore available to us we don't need to manually define the include and extend methods. We can just say:
_.extend Marionette.View.prototype,
onDomRefresh: () -> #$('.autosize').autosize()

relatedModel does not inherit from Backbone.RelationalModel -- using RequireJS and exports for circular dependency

I've run into a problem that may have to do with my lack of understanding of the use of exports / RequireJS for circular dependencies.
I'm getting the error relatedModel does not inherit from Backbone.RelationalModel.
On to the code (in CoffeeScript; I hope that's alright)...
I have two Backbone Models / RequireJS modules, FooModel and BarModel:
FooModel:
define (require) ->
Backbone = require 'backbone'
BarModel = require 'models/bar'
FooModel = Backbone.RelationalModel.extend
relations: [
type: Backbone.HasMany
key: 'theBars'
relatedModel: BarModel # <-- this is where the BB Relational error is coming from
]
return FooModel
BarModel:
define (require, exports) ->
Backbone = require 'backbone'
FooCollection = require 'collections/foos'
BarModel = Backbone.RelationalModel.extend
someFunction: ->
# uses FooCollection
# I've tried moving the require in here and getting rid of exports
exports.BarModel = BarModel
return BarModel # I've tried with and without this line, but CS just returns the last line anyway so removing it is functionally the same
I have also tried:
Extending FooModel from Backbone.Model instead of Backbone.RelationalModel and creating the theBars collection myself (in parse and in custom function). (BarModel has a HasOne relation of a another model, so I need it to still be a RelationalModel.
Is this possibly a problem with the way exports works? As far as I understand, exports just provides an object to hang module objects on so the modules are accessible elsewhere. Is the error occurring because the BarModel isn't actually a Backbone Model at the point in the FooModel code where I define relations?
Update
I seem to have solved my issue, although I'm unsure how. Can't say I'm pleased about not understanding why it's working, but I sure am pleased that it is working. Also see my comment about _.memoize below in the BarModel code.
(Before I got the code below to work, I created a workaround whereby I created the associated collection in FooModel's parse function and exported BarModel. However, the response of require 'collections/foos' returned an object like so: {FooCollection: <Backbone.Collection Object>}, i.e. it was unexpectedly wrapped in another object.)
Here's the updated code:
FooModel:
define (require) ->
Backbone = require 'backbone'
BarModel = require 'models/bar'
BarCollection = require 'collections/bars'
FooModel = Backbone.RelationalModel.extend
relations: [
type: Backbone.HasMany
key: 'theBars'
relatedModel: BarModel
collectionType: BarCollection
]
return FooModel
BarModel:
define (require, exports) ->
Backbone = require 'backbone'
BarModel = Backbone.RelationalModel.extend
someFunction: -> #this actually used to use _.memoize (sorry for the incomplete code), so maybe it would have tried to run the function argument immediately?
# uses FooCollection
FooCollection = require 'collections/foos'
return AttributeModel
Your BarModel requires 'collections/foos', correct? And I'm guessing (since there's no code for FooCollection) that the collection requires 'models/foo', because a collection needs to define it's model right? Finally, I can see from the code above that your foo model requires 'models/bar'.
In other words foos needs foo needs bar needs foos needs foo needs bar needs ...
No matter how Require decides to order that, one of those three has to be loaded before the others, which will give you problems like the one you are having.
The solution is to not load one of those three until after all three modules are loaded. For instance, what if you change:
define (require, exports) ->
Backbone = require 'backbone'
FooCollection = require 'collections/foos'
BarModel = Backbone.RelationalModel.extend
someFunction: ->
# uses FooCollection
to:
define (require, exports) ->
Backbone = require 'backbone'
BarModel = Backbone.RelationalModel.extend
someFunction: ->
FooCollection = require 'collections/foos'
# uses FooCollection
Now BarModel can load, and while someFunction is defined, it is not actually run yet, so it won't require foos and create a circular dependency. Later on, after everything is loaded and some code invokes someFunction, foos will already have had a chance to load, and the require should work.
Now I say should work because of your comment:
# I've tried moving the require in here and getting rid of exports
Again, I have to guess since I can't see your code, but I'd imagine that what happened is that nothing else depended on foos, so it never got loaded. In order for the require of foos to work synchronously inside someFunction, the foos module has to have previously been loaded.
To fix this you just need to add a dependency on foos ... only this time not in any module that requires foos (or any that require a module that requires foos, or ...).
Hope that helps.

Resources