Backbone - Trigger the parent view event from child view - javascript

I am trying to trigger the event of parent view from its child view. But it seems that the parent view's event did not gets triggered.
Following is the sample for my code:
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
<script src="http://ajax.cdnjs.com/ajax/libs/underscore.js/1.1.4/underscore-min.js"></script>
<script src="http://ajax.cdnjs.com/ajax/libs/backbone.js/0.3.3/backbone-min.js"></script>
<script>
MySubView = Backbone.View.extend({
id : "MySubView",
initialize: function() {
console.log("pro1");
this.trigger("testGo", "test");
}
});
MyView = Backbone.View.extend({
id : "MyView",
initialize: function() {
console.log("pro");
this.subview = new MySubView();
this.subview.listenTo("testGo", this.addFoo, this);
},
addFoo: function() {
console.log("ok");
alert("ok");
}
});
new MyView();
</script>
</head>
<body>
</body>
</html>
I tried to get hint from many solutions found via google search, but it seems I am got struck somewhere. Some of the options which I found are :
1/ How to trigger an event from child view to parent view in backbonejs and requirejs
2/ Backbone: How to trigger methods in a parent view

The problem is that you are using listenTo wrong.
anObject.listenTo(anotherObject, 'forSomeEvent', function () { console.log('do something'); });
So, in your case, you should do it like this:
MyView = Backbone.View.extend({
id : "MyView",
initialize: function() {
console.log("pro");
this.subview = new MySubView();
this.listenTo(this.subview, 'testGo', this.addFoo);
},
addFoo: function() {
console.log("ok");
alert("ok");
}
});
Hope it helps!

Your listenTo usage is slightly off
The signature is object.listenTo(otherObject, event, callback) so you want something like:
this.listenTo(this.subview, "testGo", this.addFoo);
Which tells this to listen to this.subview for the event testGo and call this.addFoo when the event is triggered

try this
this.listenTo(this.subview, "testGo",this.addFoo);
signature:
object.listenTo(other, event, callback)

Triggering event:
Backbone.Events.trigger('<eventname>', {data1 to be passed with event}, {data2 to be passed with event}...);
Listener:
Backbone.Events.bind('<eventname>', callback, context);

Related

Backbone window keypress event?

I know that I can use the window.keypress event
$(window).keypress(function(e) {
alert('hello world');
});
But I was wondering if there was a way to use the backbone events to catch a keypress anywhere in the window?
I cannot do it on a view, because my page will contain multiple views.
This should do the trick:
var OverlordView = Backbone.View.extend({
events: {
"keypress": "alert"
},
alert: function() {
alert('hello world')
}
});
$(function() {
new OverlordView({el: $('body')[0]})
})
Demo: http://jsfiddle.net/3obw5k8j/
Backbone's event handlers only works in its own view. Doesnt affect window context. You need to setup inside initialize i assume.
var MyView = Backbone.View.extend({
initialize : function(){
$(window).on("keypress", this.windowKeyPress)
},
windowKeyPress : function(){
// handler.
}
});

Triggering event on an element in memory when testing Backbone Views

I am writing some integration tests for my Backbone views/models/collections. When I call render on my View, it simply renders a template to it's own el property, hence the html is simply stored in memory rather than on the page. Below is a simple model, and a view with a click event bound to a DOM element:
var model = Backbone.Model.extend({
urlRoot: '/api/model'
});
var view = Backbone.View.extend({
events: {
'click #remove': 'remove'
}
render: function () {
var html = _.template(this.template, this.model.toJSON());
this.$el.html(html);
},
remove: function () {
this.model.destroy();
}
});
I am using Jasmine to write my tests. In the test below all I want to do is spy on the remove function to see if it is called when the click event is fired for the element #remove which is present in the template I pass to the view.
// template
<script id="tmpl">
<input type="button" value="remove" id="remove"/>
</script>
// test
describe('view', function () {
var view;
beforeEach(function () {
view = new view({
template: $('#tmpl').html(),
model: new model()
});
});
it('should call remove when #remove click event fired', function () {
view.$('#remove').click();
var ajax = mostRecentAjaxRequest();
expect(ajax.url).toBe('/api/model');
expect(ajax.method).toBe('DELETE');
});
});
However, as the #remove element is in memory, and it hasn't actually been added to the DOM, I'm not sure how you would simulate the click event. In fact I'm not even sure if it's possible?
It may seem a bit strange to want to do this in a test, but with my tests I am trying to test behaviour rather than implementation, and this way I don't care what is happening in between - I just want to test that if the user clicks #remove a DELETE request is sent back to the server.
Looks to me like you forgot to call render() on the view before click()ing the button. And the model needs to have an id or backbone won't actually try to make a delete call to the server. I've tested plenty of views just like that before with no problems.
I just ran a similar test against jasmine 2.0 and jasmine-ajax 2.0.
live code:
var MyModel = Backbone.Model.extend({
urlRoot: '/api/model'
});
var MyView = Backbone.View.extend({
events: {
'click #remove': 'remove'
},
initialize: function(options) {
this.template = options.template;
},
render: function () {
var html = _.template(this.template, this.model.toJSON());
this.$el.html(html);
},
remove: function () {
this.model.destroy();
}
});
specs:
describe("testing", function() {
var view;
beforeEach(function() {
jasmine.Ajax.install();
view = new MyView({
template: '<input type="button" value="remove" id="remove"/>',
model: new MyModel({id: 123})
});
view.render();
});
it('should call remove when #remove click event fired', function () {
view.$('#remove').click();
var ajax = jasmine.Ajax.requests.mostRecent();
expect(ajax.url).toBe('/api/model/123');
expect(ajax.method).toBe('DELETE');
});
});

Do something when ember is done with DOM updates

I want to do stuff when ember bindings have synchronized and the DOM is again up to date.
I have tried with a callback from the function that manipulates the binded model, DOM is not updated when callback is executed.
I have tried with an observer directly on the model, DOM is not updated when the observer is executed.
I have tried with an observer on the binding, DOM is not updated when the observer is executed.
e.g.
App.view = Ember.View.extend({
modelBinding: 'App.model',
modelChanged : function() {
window.scrollTo(0, document.body.scrollHeight);
}.observes('model'),
getMore: function(event) {
App.set('model', "somethingnew");
}
});
When I fire the "gotMore", I update the model, and when the model is updated and its changes have been rendered I want to scroll down.
In none of the ways I've tried have I been able to get the new scrollHeight. It is set a few ms after these events.
Here's an example on jsFiddle:
http://jsfiddle.net/kcjzw/15/
The correct way to do this is documented here:
http://emberjs.com/api/classes/Ember.run.html#method_next
modelChanged : function() {
Ember.run.scheduleOnce('afterRender', this, function() {
window.scrollTo(0, document.body.scrollHeight);
$('body').append('New scroll height: '+document.body.scrollHeight);
});
}.observes('content')
Use Ember.run.next
https://github.com/emberjs/ember.js/blob/master/packages/ember-metal/lib/run_loop.js#L531-566
App.view = Ember.View.extend({
modelBinding: 'App.model',
modelChanged : function() {
Ember.run.next(myContext, function(){
// code to be executed in the next RunLoop, which will be scheduled after the current one
window.scrollTo(0, document.body.scrollHeight);
});
}.observes('model'),
getMore: function(event) {
App.set('model', "somethingnew");
}
});
Update
Take a look at this: http://jsfiddle.net/ud3323/hZ7Vx/
What you need is to run your code after the runloop that renders the Ember.CollectionView that the {{each}} helper would create.
JavaScript:
App = Ember.Application.create();
App.model = Ember.Object.create({
items: [1]
});
App.view = Ember.Handlebars.EachView.extend({
contentBinding: 'App.model.items',
itemViewClass: Ember._MetamorphView.extend({
templateName: 'the_template'
}),
modelChanged : function() {
Ember.run.next(this, function(){
window.scrollTo(0, document.body.scrollHeight);
$('body').append('New scroll height: '+document.body.scrollHeight);
});
}.observes('content'),
theAction: function(event) {
App.controller.doStuffToModel();
}
});
App.controller = Ember.Object.create({
doStuffToModel : function() {
App.model.set('items', [1,2,3,4,5]);
}
});
Handlebars:
<script type="text/x-handlebars" data-template-name="the_template">
<div style="height:200px;"></div>
</script>
<script type="text/x-handlebars">
{{view App.view}}
</script>​

Backbone.js View can't unbind events properly

I have some Backbone.js code that bind a click event to a button,
and I want to unbind it after clicked, the code sample as below:
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"cancel"
},
cancel:function(){
console.log("do something...");
this.$(".button").unbind("click");
}
});
var view = new AppView();
However the unbind is not working, I tried several different way and end up binding event in initialize function with jQuery but not in Backbone.events model.
Anyone know why the unbind is not working?
The reason it doesn't work is that Backbonejs doesn't bind the event on the DOM Element .button itself. It delegates the event like this:
$(this.el).delegate('.button', 'click', yourCallback);
(docs: http://api.jquery.com/delegate)
You have to undelegate the event like this:
$(this.el).undelegate('.button', 'click');
(docs: http://api.jquery.com/undelegate)
So your code should look like:
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"cancel"
},
cancel:function(){
console.log("do something...");
$(this.el).undelegate('.button', 'click');
}
});
var view = new AppView();
Another (maybe better) way to solve this is to create a state attribute like this.isCancelable now everytime the cancel function is called you check if this.isCancelable is set to true, if yes you proceed your action and set this.isCancelable to false.
Another button could reactivate the cancel button by setting this.isCancelable to true without binding/unbinding the click event.
You could solve this another way
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"do"
},
do:_.once(function(){
console.log("do something...");
})
});
var view = new AppView();
underscore.js once function ensures that the wrapped function
can only be called once.
There is an even easier way, assuming you want to undelegate all events:
this.undelegateEvents();
I like bradgonesurfing answer. However I came across a problem using the _.once approach when multiple instances of the View are created. Namely that _.once would restrict the function to be called only once for all objects of that type i.e. the restriction was at the class level rather than instance level.
I handled the problem this way:
App.Views.MyListItem = Backbone.View.extend({
events: {
'click a.delete' : 'onDelete'
},
initialize: function() {
_.bindAll(this);
this.deleteMe = _.once(this.triggerDelete);
},
// can only be called once
triggerDelete: function() {
console.log("triggerDelete");
// do stuff
},
onDelete:(function (e) {
e.preventDefault();
this.deleteMe();
})
});
Hopefully this will help someone
you can simply use object.off, the code below is work for me
initialize:function () {
_.bindAll(this, 'render', 'mouseover', 'mouseout', 'delete', 'dropout' , 'unbind_mouseover', 'bind_mouseover');
.......
},
events: {
'mouseover': 'mouseover',
'unbind_mouseover': 'unbind_mouseover',
'bind_mouseover': 'bind_mouseover',
.....
},
mouseover: function(){
$(this.el).addClass('hover');
this.$('.popout').show();
},
unbind_mouseover: function(){
console.log('unbind_mouseover');
$(this.el).off('mouseover');
},
bind_mouseover: function(){
........
},

Backbone basic app, is this how it should be done?

Hope you can have a quick look at what I'm doing here. Essentially, am I doing it right?
Live demo of it here too: http://littlejim.co.uk/code/backbone/messing-around/
I just wanted to get a solid understanding in Backbone before I go too wild. So this is a simple demonstration of creating a collection from a JSON object, passing it to a view and handling simple events. But am I approaching this right? What can I do that's better?
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Showing a simple view with events</title>
<script type="text/javascript" src="../../media/scripts/jquery-1.5.1.min.js"></script>
<script type="text/javascript" src="../../media/scripts/underscore-min.js"></script>
<script type="text/javascript" src="../../media/scripts/backbone-min.js"></script>
<script type="text/javascript" src="application.js"></script>
</head>
<body>
<header>
<h1>Showing views from a collection and basic events</h1>
<p>The list below is made from JSON, passed to the view as a collection and has basic events</p>
</header>
<article>
</article>
</body>
</html>
Here is the JavaScript I currently have. I just need to know if I'm approaching this correctly?
window.App = {
// namespaces
Controller: {},
Model : {},
Collection : {},
View : {},
// code that starts when the app is first fired
initialize : function () {
var collection = new App.Collection.Inputs([
{title: "Item 1"},
{title: "Item 2"},
{title: "Item 3"}
]);
var view = new App.View.InputSet({collection: collection});
$('article').html(view.render().el);
}
}
/*
Collection: Inputs */
App.Collection.Inputs = Backbone.Collection.extend();
/*
View: _Input */
App.View._Input = Backbone.View.extend({
events: {
"click a": "close"
},
// called as soon as a view instance is made
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render", "close");
},
// backbone required method, which renders the UI
render: function() {
// this is using underscore templating, which can be passed context
$(this.el).html(_.template('<p><%=title%> [close]</p>', this.model.toJSON()));
return this;
},
close: function() {
// removes the UI element from the page
$(this.el).fadeOut(300);
return false; // don't want click to actually happen
}
});
/*
View: InputSet, uses _Input */
App.View.InputSet = Backbone.View.extend({
events: {
'click a': 'clear'
},
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render");
},
// backbone required method, which renders the UI
render: function() {
var that = this;
views = this.collection.map(function(model) {
var view = new App.View._Input({model: model});
$(that.el).append(view.render().el);
return view;
});
$(that.el).append('[clear]');
return this;
},
clear: function() {
$(this.el).find('p').fadeOut(300);
}
});
// wait for the dom to load
$(document).ready(function() {
// this isn't backbone. this is running our earlier defined initialize in App
App.initialize();
});
This looks fine to me. However, I found that things can get tricky once you start doing non-trivial stuff: complex views, nested collections etc.
One thing that could be done differently is that instead of generating input views using collection.map you could bind the collection's add event to a function that generates an _Input view for that item in the collection instead. So you'd have something like this in your InputSet view:
initialize: function() {
_.bindAll(this, "addInput", "removeInput");
this.collection.bind("add", this.addInput);
this.collection.bind("remove", this.removeInput);
}
addInput: function(model) {
var view = new App.View._Input({model: model});
$(this.el).append(view.render().el);
}
I looks good to me - really the only thing I would suggest is that you bind the collection's 'change' event to _Input.render that way changes to your collection automatically re-render the view:
// called as soon as a view instance is made
initialize: function() {
_.bindAll(this, "render", "close");
this.collection.bind('change', this.render);
},
Other than that I think it looks good!

Categories