I have the following backbone view:
class Observation extends Backbone.Model
class Observations extends Backbone.Collection
model: Observation
constructor: ->
#url = _observationsUrl
class ObservationsView extends Backbone.View
el: $('#observations')
initialize: ->
_.bindAll #
#model.bind 'changed', #render
#model.view = #
that = #
#model.fetch {
success: ->
alert('success')
that.model.trigger 'changed'
}
render: =>
alert('rendering baby')
class ObservationsController extends Backbone.Controller
initialize: ->
observations = new Observations()
observationsView = new ObservationsView(model: observations)
I am binding the model's changed event to the render method of the ObservationsView. The model is a backbone collection.
The fetch is working successfully but the changed event is not being fired. I am trying the manual trigger out of desperation.
Can anyone see what I am doing wrong?
The event isn't called 'changed'. The event triggered after the model's collection has been refreshed from the server is 'refresh'.
The 'change' event is actually more complicated. It's an event on a model that's triggered whenever you call .set(), and it always includes the attribute, so you'd write things like:
this.model.bind('change:username', _.bind(this.update_username_display, this))
As always, the backbone.js source code is eminently readable.
Related
I'm writing a Backbone program in Typescript, in which I am not able to initialize any events. Here's a test class that I've created to fix the problem. The function start() is not being called when the div is clicked on.
class TestView extends Backbone.View{
events = {
"click #testDiv" : "start"
}
start(){
console.log("Clicked");
}
constructor(options?){
super(options);
}
render(){
$root.html(getNewDiv("testDiv"));
$("#testDiv").css("height", 100).css("width", 100).css("background-color", "green");
console.log("Rendered");
return this;
}
}
function getNewDiv(id:string) {
return "<div id = \"" + id + "\"></div>"
}
new TestView().render();
Here's the console output:
Rendered
Here's the backbone typescript definition that I'm using:
https://github.com/borisyankov/DefinitelyTyped/blob/master/backbone/backbone.d.ts
Here's the CDN location for backboneJS
Minified : http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.0/backbone-min.js
Non-Minified : http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.0/backbone.js
I'm concerned if my syntax is right or this has something to do with the Typescript definition of Backbone.
UPDATE
The answer shown by Kevin Peel below is throwing an error, because in the Typescript definition file, "events" is defined as a function ( eg. events()). If I just create an "events" function, I get the error - "Uncaught TypeError: Object [object Object] has no method 'off'". If I use the getter method (eg. get events(){}), then nothing really happens on actually firing the event.
Using delegate Events
I tried to use the delegateEvents() function in Backbone.View which creates an error:
constructor(options:any, question:Question, div:JQuery) {
this.delegateEvents({
"click" : "clicked"
});
}
Error:
Uncaught TypeError: Object [object Object] has no method 'off' backbone.js:1082
h.extend.undelegateEvents backbone.js:1082
h.extend.delegateEvents backbone.js:1059
The problem is with where TypeScript defines the events. When you define events like this:
class TestView extends Backbone.View {
events = {
"click #testDiv": "start"
}
// ...
}
What TypeScript does is attaches events as a property after initializing the instance. So when Backbone is initializing the view, events haven't yet been attached, so Backbone isn't able to bind them.
Unfortunately, there doesn't seem to be a nice way to get TypeScript to assign events to the prototype, so you need to hack around it in some way.
The absolute easiest way is by setting the events on the prototype yourself:
class TestView extends Backbone.View {
// Get rid of the events declaration in here
// ...
}
TestView.prototype.events = {
"click #testDiv": "start"
}
A second possible way to get around the issue is using the getter functionality of TypeScript as suggested in this StackOverflow answer.
Finally, a third way would be to bind your events each time you render. This is another easy way to get around the TypeScript problem, but it might not be the best solution if the view gets rendered many times.
class TestView extends Backbone.View{
events = {
"click #testDiv" : "start"
}
// ...
render(){
$root.html(getNewDiv("testDiv"));
// Here is where you'll bind the events using Backbone's delegateEvents
this.delegateEvents();
$("#testDiv").css("height", 100).css("width", 100).css("background-color", "green");
console.log("Rendered");
return this;
}
}
// ...
new TestView().render();
I know is an old question but there is now another possibility, using es7 decorators, similar to what angular2 does:
#component({
events: {
"click #testDiv" : "start"
}
})
class TestView extends Backbone.View{
start(){
console.log("Clicked");
}
render(){
// ...
}
}
Decorator file
export function component(definition: any) {
return function (constructor: any) {
Object.assign(constructor.prototype, definition)
}
}
This way, all properties defined in the component decorator will be attached to the class prototype and will be available at instantiation time.
Another option besides using a class/constructor decorator could be to use a method decorator on the preinitialize method if you wish to have all the code inside the class braces like this:
class TestView extends Backbone.View {
#component({
events: {
"click #testDiv" : "start"
}
})
preinitialize(){}
start(){
console.log("Clicked");
}
render(){
// ...
}
}
Decorator file
export function component(definition: any) {
return function (target: Object, methodName: string, descriptor: TypedPropertyDescriptor<Function>) {
Object.assign(target, definition)
}
}
This is a code fragment of my ItemView:
class List.GridRow extends Backbone.Marionette.ItemView
tagName: 'tr'
triggers:
'click': 'row:clicked'
Then in my Composite view I am doing this:
class List.GridView extends Backbone.Marionette.CompositeView
template: 'mapping/list/templates/grid'
itemView: List.GridRow
itemViewContainer: 'tbody'
initialize: (options) ->
#listenTo #, 'itemview:row:clicked', (itemView, data) -> #rowClicked(itemView, data)
rowClicked: (clickedItemView, data) =>
# I need the original event information to check of the ctrl or shift key was pressed?
#if !e.ctrlKey && !e.shiftKey
What I am trying to do here is to pass the original event information into the trigger handler but I haven't figure it out yet? Is there a way to do this with Marionette? Am I missing something?
Thanks!
The intent of the triggers is to provide a minimal event configuration for minimal needs within the view, while also preventing leaky abstractions. Passing the original event args out of the view breaks the abstraction of the view, which is what should be in control of the event args.
If you need the control and shift key information, you'll want to avoid the triggers configuration and use standard events that publish the info you need.
class List.GridRow extends Backbone.Marionette.ItemView
tagName: 'tr'
events:
'click': 'rowClicked'
rowClicked (e) ->
#trigger "row:clicked",
control: e.ctrlKey,
shift: e.shiftKey
events: {
'click .pdf_preview' : 'onPreviewPdfClick',
'click a.next' : '_nextPointer',
'click a.prev' : '_prevPointer',
'mouseleave .mediapop-item-container' : 'onMouseLeave',
'mouseenter .mediapop-item-container' : 'onMouseEnter',
// Editable events
'click &.editable .dialog-header h3' : 'showEditTitleForm' <--- this
},
I create a popup view that has an edit mode. While I could simply extend this view with an editable version, what I need is very basic, so instead I set it up so if you pass this.options.editable the view.el element will have an "editable" class on it.
I am wondering if there is a way for me to specify that selector inside of the event object. That way it won't event trigger if this.options.editable is not true. If Backbone supported a SASS style selector syntax then the above would work. The only alternative is to check for this.$el.hasClass('ediable') within showEditTitleForm.
I know if you set a selector like this:
'click' : 'myAction',
The click event will be applied to the this.el container. I'm looking to only apply the callback if the additional class is there.
Is it possible to do something similar to the above without adding a check within the callback method?
Edit: typo
You can always modify events before delegateEvents is called, just be careful not to modify the events attached to the view's prototype. Something like this:
initialize: function() {
if(this.options.editable) {
this.events = _({}).extend(this.events, {
'click .dialog-header h3': 'showEditTitleForm'
});
}
}
Note the _({}).extend(...), that will merge this.events (which comes from the prototype) and the extra event into an empty object so that you don't accidentally alter the events in the prototype.
Demo: http://jsfiddle.net/ambiguous/dNYXN/
I have a Marionette.ItemView that uses a CSS class to style new items differently:
class Happenator.Views.Option extends Backbone.Marionette.ItemView
tagName: 'li'
className: =>
return 'new' if #model.isNew()
initialize: ->
#bindTo #model, "change", -> #render()
When the model is saved and updated, everything refreshes but the 'new' class remains on the 'li'. Is there a good way to update the enclosing tags' class on updates?
Yes, the 'new' class remains on the 'li', because in fact Backbone uses the className property only right before an initialize method call. Take a look at this answer for more explanations.
But why don't use jQuery .toggleClass or .removeClass? Something like
render: =>
#$el.html(#template(#model.toJSON()))
unless #model.isNew()
#$el.removeClass('new')
http://jsfiddle.net/GX8WJ/21/
I have the following class that extends Backbone.View, I want all my backbone views to inherit from this class:
class BaseView
constructor: (options) ->
#bindings = []
Backbone.View.apply(#, [options])
_.extend(BaseView.prototype, Backbone.View.prototype, {
#etc. tec.
BaseView.extend = Backbone.View.extend
I can then extend my own view like this:
class BusinessUnitsView extends BaseView
initialize: (options) ->
This all works fine if they are in the same file but if I separate BaseView into a different file, I get an error message:
BaseView is undefined
How can I keep the BaseView in a different file and use it to extend my custom views?
Put this under BaseView.extend = Backbone.View.extend
#.BaseView = BaseView
it makes your BaseView global accessible
I always declare my classes like this and it works great
class BaseView extends Backbone.View
#.BaseView = BaseView