Marionette controller.listenTo not capturing event - marionette

I'm having a weird problem with some Backbone Marionette controller in a sub-application module.
I cannot make it to work capturing one of its view events with the "controller.listenTo(view, 'event', ...)" method, although the "view.on('event',...)" works no problem.
Here is the sample code for the module views :
MyApp.module("SubApp.Selector", function (Selector, App, Backbone, Marionette, $, _) {
"use strict";
// Category Selector View
// ------------------
// Display a list of categories in the selector
// Categories list item
Selector.CategoryOption = Marionette.ItemView.extend({
template: "#my-category-template",
tagName: "option",
onRender: function () { this.$el.attr('value', this.model.get('id')); }
});
// Categories list container
Selector.CategoryListView = Marionette.CompositeView.extend({
template: "#my-category-list-template",
tagName: "div",
id: "categories",
itemView: Selector.CategoryOption,
itemViewContainer: "select",
events: {
'change #category_items': 'categoryClicked'
},
categoryClicked: function() {
var catID = this.$('#category_items').val();
console.log("category clicked "+catID);
this.trigger("category:changed", catID);
}
});
// Selector Component Controller
// -------------------------------
Selector.Viewer = Marionette.Controller.extend({
initialize: function (_options) {
this.region = _options.region;
this.collection = _options.collection;
},
show: function () {
var view = new Selector.CategoryListView({ collection: this.collection });
this.listenTo(view, 'category:changed', this.categoryChanged);
//view.on('category:changed', this.categoryChanged, this);
this.region.show(view);
},
categoryChanged: function (_category) {
console.log("category changed in controller");
}
});
});
Is there anything I got wrong about event listening from a controller object ?
Shouldn't I use the listenTo syntax as is seems to be recommended widely for proper event handling and listener destruction ?

Related

Marionette Layout: trigger event on child view

I have a layout view, with an itemView inside it. I have an event in my item view that triggers a save function. Inside that save function I would like to trigger another event that the layout captures.
So in the code below, in the onClickSave modelSaveSuccess I'd like to trigger a function in the parent layout, I have tried this.methodInParent() but it doesnt work
childView
define(["marionette", "underscore", "text!app/templates/client/form.html", "app/models/client"], function(Marionette, _, Template, Model) {
"use strict"
return Backbone.Marionette.ItemView.extend({
events: {
"submit #saveClient": "onClickSave"
},
onClickSave: function(ev) {
ev.preventDefault()
return this.model.save({}, {
success: function() {
console.log('success - trigger ')
},
error: function(request, error) {
console.log(error.responseText)
}
})
}
})
})
A good way to do it without introducing heavy coupling is to use Marionette's event aggregator as in the linked exemple if you use Backbone.Marionette.application.
// in your view
...
success: function() {
app.vent.trigger('myview:modelsaved');
}
...
// in your layout initialize()
...
app.vent.on('myview:modelsaved', function(){
console.log('model saved in itemView');
});
...
If you don't use Backbone.Marionette.Application you can always create your own Backbone.Wreqr.EventAggregator.

Handle tapStart Event on a button

I have an app with a carousel. On all of the carousel pages there are elements such as buttons and datepickers. I would like to handle the tapStart event on each of these elements using Sencha Touch but I haven't been able to find anything to allow me to do this.
Does anyone have an idea?
UPDATE
I asked this question on the Sencha Forums as well. Here is the link to the Sencha Forum thread: http://www.sencha.com/forum/showthread.php?262804-Handle-tapStart-Event-on-a-button&p=963782#post963782
You can try using touchstart which can be bound to any element including button
I figured out a solution to my problem with help from the Sencha Touch Forums.
First I used the initConfig function to initialize my configuration of my container.
Ext.define('MyApp.view.ViewName', {
...
// Very Important, this is what I use in the controller to handle the events
xtype: 'myxtype',
...
initConfig: function () {
var me = this;
this.config = {
...
items: {
...
{
xtype: 'button',
...
listeners: {
element: 'element',
// This is where my code handles the tapstart
// (touchstart) event
touchstart: function () {
// Fire an event on the controller (me)
me.fireEvent('buttondown');
}
}
},
...
}
}
this.callParent([this.config]); // Very Important when using initConfig
}
});
Then, in my controller I added this code:
Ext.define('MyApp.controller.MainController', {
...
config: {
views: [
'ViewName',
...
],
...
},
...
init: function () {
this.control({
'myxtype': {
buttondown: this.myFunction
}
})
},
myFunction: function () {
// Do something
}
});

BackboneJS and Model/Collection

Have json-data like this:
{ tournaments: [ {
tournament_id: "..."
tournament_name: "..."
events: [ {
event_id: ...
event_name: ....
param : [ {
param_a :
param_b : ..
subparan : [ {
sub_1: 1
sub_2 : 2...
So. I don't understand - how to it implement into BackBone Collection/Model style?
How to handle change sub_1? - Made Collection of Collection of Collection?
Simpliest way described in backbone tutorial:
var Events = Backbone.Collection.extend({
initialize: function(){
this.params = new Params()
}
})
var Tournaments = Backbone.Model.extend({
initialize: function(){
this.events = new Events()
}
})
var tournaments = new Tournaments()
You can continue nesting by you needs. When I was working on similar task I wrap each collection in model representing collection state and change itself in answer of collection events. This allows not to asking nested collections about its state having actual state in model.
var CollModel = Backbone.Model.extend({
defaults: {
state = ''//or list or dict or whatever
},
initialize: function(){
this.items = new Backbone.Collection();
this.items.on('all', this.setState, this)
},
setState: function(){
this.set(
'state',
this.items.reduce(function(state, item){
/*calculate state*/
}, '')
)
},
info: function(){
return this.get('state')
}
})
So you can nest collection-models with similar technic and read their state directly through instance.info() depends on how you calculate it. Your top model state will be updated from cascade updates of underneath models-collections.

App structure and App.vent.bind

I have two Apps:
A SrvLinkApp that has a Link Model (a sockjs connection to the server).
A ChatApp that has a chatView view, a ChatEntry Model, and a ChatCollection.
When I receive a msg form the server I trigger a 'chat:server:message', payload event with:
App.vent.trigger('chat:server:message', payload)
In my ChatApp I listen for this event, transform the payload into a ChatEntry and then add it to the ChatCollection witch is referenced by the ChatView.
Where should I add the binding? I only have the reference to the collection in the initialize part:
App.vent.bind("chat:server:msg", function(msg) {})
Plan A
Foo.module("ChatApp", function(ChatApp, App, Backbone, Marionette, $, _) {
App.addRegions({
chatRegion: "#chat-region",
});
MsgEntry = Backbone.Model.extend({});
MsgCollection = Backbone.Collection.extend({
model: MsgEntry
})
MsgView = Backbone.Marionette.ItemView.extend({
template: '#chat-entry-template',
});
MsgListView = Backbone.Marionette.CompositeView.extend({
itemView: MsgView,
itemViewContainer: "#chat-messages",
template: "#chat",
....
});
ChatApp.addInitializer(function() {
var msgCollection = new MsgCollection();
var msgListView = new MsgListView({collection: msgCollection});
// render and display the view
App.chatRegion.show(msgListView);
// App Events listeners
// --------------------
// New chat message from server
App.vent.bind("chat:server:msg", function(msg) {
// create an entry and add it to our collection
console.log(msgCollection);
});
});
});
or Plan B
Foo.module("ChatApp", function(ChatApp, App, Backbone, Marionette, $, _) {
App.addRegions({
chatRegion: "#chat-region",
});
// App Events listeners
// --------------------
// New chat message from server
App.vent.bind("chat:server:msg", function(msg) {
// create an entry and add it to our collection
console.log(ChatApp.msgCollection);
});
MsgEntry = Backbone.Model.extend({});
MsgCollection = Backbone.Collection.extend({
model: MsgEntry
})
MsgView = Backbone.Marionette.ItemView.extend({
template: '#chat-entry-template',
});
MsgListView = Backbone.Marionette.CompositeView.extend({
itemView: MsgView,
itemViewContainer: "#chat-messages",
template: "#chat",
....
});
ChatApp.addInitializer(function() {
var msgCollection = new MsgCollection();
var msgListView = new MsgListView({collection: msgCollection});
// HERE //
ChatApp.msgCollection = msgCollection;
// END HERE //
App.chatRegion.show(msgListView);
});
});
Or is there other ways to access the collection ?
I don't think either of these is bad. I use both ideas at various times. It largely depends on other circumstances within your app, what the possibilities of are of having multiple collections to deal with, etc.
Personally, I'd go with A because it keeps everything within the module, and doesn't leave the collection open to getting clobbered or forgotten about (memory leak) on the global app object.

Attach the events ajaxStart() and ajaxStop() only to the current backbone view

I'm using backbone for an app that I'm building. In this app, I have a master view which render a template with 2 other views inside. One header view and another one with some content. The header view is just used to interact with the content view and has specific functions too.
In the header template and content template I have the same piece of code, an hidden DIV with a loader image that is displayed when an ajax call is made. The problem I have is that when I load the app for the first time (or when I refresh the content view), the content view is loading some data from an ajax request, but the loader is showing up in both the header and the content template (like if the ajaxStart() was a global event not attached to the view.
Here is the content view setup:
App.View.Content = Backbone.View.extend({
type:'test',
template: twig({
href: '/js/app/Template/Content.html.twig',
async: false
}),
block:{
test:twig({
href: '/js/app/Template/block/test.html.twig',
async: false
})
},
list:[],
showLoader: function(el){
console.log('loader: ', $('#ajax_loader', el));
$('#ajax_loader', el).show();
console.log('Ajax request started...');
},
hideLoader: function(el){
$('#ajax_loader', el).hide();
console.log('Ajax request ended...');
},
initialize: function(params)
{
this.el = params.el;
this.type = params.type || this.type;
var self = this;
this.el
.ajaxStart(function(){self.showLoader(self.el);})
.ajaxStop(function(){self.hideLoader(self.el);});
this.render(function(){
self.list = new App.Collection.ListCollection();
self.refresh(1, 10);
});
},
refresh:function(page, limit)
{
var self = this;
console.log('Refreshing...');
$('#id-list-content').fadeOut('fast', function(){
$(this).html('');
});
this.list.type = this.type;
this.list.page = page || 1;
this.list.limit = limit || 10;
this.list.fetch({
success: function(data){
//console.log(data.toJSON());
$.each(data.toJSON(), function(){
//console.log(this.type);
var tpl_block = self.block[this.type];
if (tpl_block != undefined) {
var block = tpl_block.render({
test: this
});
$(block).appendTo('#id-list-content');
}
});
$('#id-list-content').fadeIn('fast');
}
});
},
render: function(callback)
{
console.log('Rendering list...');
this.el.html(this.template.render({
}));
if (undefined != callback) {
callback();
}
}
});
As you can see I'm using an ugly piece of code to attach the ajaxStart / ajaxStop event:
this.el
.ajaxStart(function(){self.showLoader(self.el);})
.ajaxStop(function(){self.hideLoader(self.el);});
I use to have it like this:
this.el
.ajaxStart(self.showLoader())
.ajaxStop(self.hideLoader());
But for whatever reason that still undefined on my end, this.el was not defined in the showLoader() and hideLoader().
I was thinking that ajaxStart() and ajaxStop() was attached to the this.el DOM, and that only this view would be able to listen to it. But my headerView which has exactly the same setup (except for the twig template loaded) apparently receive the event and show the loader.
To be sure of this behavior, I've commented out the showLoader() in the content view, and the loader still show up in the header view.
I don't know what I'm doing wrong :(
EDIT (after answer from "mu is too short"):
my content view does now looks like this:
showLoader: function(){
//this.$('#ajax_loader').show();
console.log('Ajax request started...');
},
hideLoader: function(){
this.$('#ajax_loader').hide();
console.log('Ajax request ended...');
},
initialize: function(params)
{
var self = this;
console.log(this.el);
_.bindAll(this, 'showLoader', 'hideLoader');
this.$el
.ajaxStart(this.showLoader)
.ajaxStop(this.hideLoader);
this.render(function(){
self.list = new App.Collection.List();
self.refresh(1, 10);
});
},
...
render: function(callback)
{
console.log('Rendering post by page...');
this.$el.html(this.template.render({
}));
if (undefined != callback) {
callback();
}
}
and my header view:
...
showLoader: function(){
this.$('#ajax_loader').show();
//console.log('Ajax request started...');
},
hideLoader: function(el){
this.$('#ajax_loader').hide();
console.log('Ajax request ended...');
},
initialize: function(params)
{
var self = this;
_.bindAll(this, 'showLoader', 'hideLoader');
this.$el
.ajaxStart(this.showLoader)
.ajaxStop(this.hideLoader);
this.models.Link = new App.Model.Link();
this.render();
},
render: function(callback)
{
this.$el.html(this.template.render({
data: []
}));
if (undefined != callback) {
callback();
}
}
...
But the loader still showing up in the header view template
PS: this.showLoader() was not a typo as I wanted to call the function within the current backbone view.
The context (AKA this) for a JavaScript function depends on how the function is called, not on the context in which the function is defined. Given something like this:
var f = o.m;
f();
When you call o.m through the plain function f, this inside o.m will usually be the global context (window in a browser). You can also use apply and call to choose a different this so this:
f.call(o);
would make this the o that you'd expect it to be. I should mention that you can force your choice of this using bind in most JavaScript environments but I don't want to get too sidetracked.
The point is that this:
this.el
.ajaxStart(this.showLoader)
.ajaxStop(this.hideLoader);
isn't enough to ensure that showLoader and hideLoader will run in the right context; I'm also assuming that the parentheses you had at the end of showLoader and hideLoader were just typos.
The most common way to force a context in a Backbone application is to use _.bindAll in your initialize:
initialize: function(params) {
_.bindAll(this, 'showLoader', 'hideLoader');
//...
That essentially replaces this.showLoader and this.hideLoader with something that's, more or less, equivalent to your wrappers:
function() { self.showLoader(self.el) }
Once you have that _.bindAll in place, this:
this.el
.ajaxStart(this.showLoader)
.ajaxStop(this.hideLoader);
will work fine.
BTW, you don't need to do this:
this.el = params.el;
in your initialize, Backbone does that for you:
constructor / initialize new View([options])
[...] There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName and attributes.
And you don't need to do things like this:
$('#ajax_loader', el).show();
either, Backbone gives you a $ method in your view that does the same thing without hiding the el at the end of the argument list; doing it like this:
this.$('#ajax_loader').show();
is more idiomatic in Backbone.
Furthermore, this.el won't necessarily be a jQuery object so don't do this:
this.el.html(this.template.render({ ... }));
in your render, use the cached this.$el instead:
this.$el.html(this.template.render({ ... }));

Resources