Marionette Layout: trigger event on child view - events

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.

Related

Routing in Extjs with DeftJs

deftjs looks really promising as it adds exactly the necessary things I missed in the MVC implementation of ExtJs.
What I actually miss is a functionality that makes routing possible/ easy. Extjs has a Ext.ux.Router functionality but I formerly used code like this with help of this lib:
initRoutes: function () {
var me = this;
Log.debug('Books.controller.App: initRoutes');
//use PATH.JS library until ExtJs supports routing as Sencha Touch 2.0 does. (see utils\Path)
Path.map("#/home").to(function () {
me.getController('Home').index();
});
Path.map("#/trackingsheet").to(function () {
me.getController('TrackingSheet').index();
});
Path.root('#/home');
Path.listen();
}
As the procedure of creating the crucial parts in deftjs is now exactly the other way around (view creates the controller) I certainly cannot refer to a controller's method and instantiate the view and make it the visible one. I have a pretty simple card layout here - what means only one view can be visible at a time, it is not necessary to go any deeper like this (e.g. make a task pane visible or the like).
What is the preferred way to do it?
I can think of making the Viewport a view factory having some methods like the controller before.
Thanks,
da5id
I solved this problem by using Ext.util.History class in a history context class that can raise an event when the hash changes:
Ext.define('myApp.context.HistoryContext', {
mixins: {
observable: 'Ext.util.Observable'
},
constructor: function(config) {
var me = this;
if (config == null) {
config = {};
}
this.initConfig(config);
Ext.util.History.add('home');
//init Ext.util.History; if there is a hash in the url,
//controller will fire the event
Ext.util.History.init(function(){
var hash = document.location.hash;
me.fireEvent('tokenChange', hash.replace('#', ''));
});
//add change handler for Ext.util.History; when a change in the token occurs,
//this will fire controller's event to load the appropriate content
Ext.util.History.on('change', function(token){
me.fireEvent('tokenChange', token);
});
this.mixins.observable.constructor.call(this);
this.addEvents('tokenChange');
return this.callParent(arguments);
}
});
Then you can inject this context in to your controller, and observe the token change, and implement the action in dispatch method:
Ext.define('myApp.controller.HomeController', {
extend: 'Deft.mvc.ViewController',
inject: [
'historyContext'
],
control: {
appContainer: {},
home: {
click: 'addHistory'
},
about: {
click: 'addHistory'
}
},
observe: {
historyContext: {
tokenChange: "dispatch"
}
},
init: function() {
return this.callParent(arguments);
},
switchView: function(view) {
//change this to get the cards for your card layout
this.getAppContainer().add(Ext.ComponentMgr.create({
xtype : view,
flex : 1
}));
},
addHistory: function(btn) {
var token = btn.itemId;
Ext.util.History.add(token);
},
dispatch: function(token) {
// switch on token to determine which content to load
switch(token) {
case 'home':
this.switchView('view-home-Index');
break;
case 'about':
this.switchView('view-about-Index');
break;
default:
break;
}
}
});
This should be ok for the first level routing (#home, #about), but you need to implement your own mechanism to fetch the token for the second and third level routes. (#home:tab1:subtab1) You can possibly create a service class that can handle fetching the hash and inject the service to each controllers to dispatch.
For further discussion in this topic, go to https://github.com/deftjs/DeftJS/issues/44

Marionette controller.listenTo not capturing event

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 ?

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({ ... }));

jplayer+Ajax inserted content

I am using jPlayer to play audio files.
If I use the player on content, which is privided, when the page gets loaded, it works without any problems.
I also need it for HTML which is inserted by AJAX. Here it does not work. It seems, that the ready event is not triggered.
I wrote a function, which can be executed by click(). In that way, I can click it manually, when the HTML which contains the player is fully loaded. Here I have the same problem: The ready event is not triggered.
This is my function which works on non ajax inserted players fine:
$('.jp-jplayer').each(function () {
var src = $(this).attr('data-src');
var id = $(this).attr('id');
var post_id = $(this).attr('data-id');
alert('beg');
$('#' + id).jPlayer({
ready: function () {
$(this).jPlayer('setMedia', {
mp3: "/prelisten/_lofidl/change_of_heart_full_lofi.mp3",
});
alert('#' + id);
},
swfPath: "/wp-content/themes/Dark_3Chemical_DE_mit_Pagenavi/Dark_3Chemical_DE/audioplayer/js",
//////ERRRROOOOOR
solution: "flash, html",
supplied: "mp3",
wmode: "window",
cssSelectorAncestor: "#jp_container_" + post_id,
play: function () { // To avoid both jPlayers playing together.
$(this).jPlayer("pauseOthers");
},
repeat: function (event) { // Override the default jPlayer repeat event handler
if(event.jPlayer.options.loop) {
$(this).unbind(".jPlayerRepeat").unbind(".jPlayerNext");
$(this).bind($.jPlayer.event.ended + ".jPlayer.jPlayerRepeat", function () {
$(this).jPlayer("play");
debug($(this));
});
} else {
$(this).unbind(".jPlayerRepeat").unbind(".jPlayerNext");
$(this).bind($.jPlayer.event.ended + ".jPlayer.jPlayerNext", function () {
//$("#jquery_jplayer_4858").jPlayer("play", 0);
});
}
},
});
$("#jplayer_inspector").jPlayerInspector({
jPlayer: $('#' + id)
});
});
Currently I am setting the src manually to exclude any possible errors here.
How can I get this function running on AJAX inserted content?
EDIT:
This is the code, which fetches the html including the players:
$.get('/query_posts.php', {
paged: _page,
cats: cols
}, function(data) {
$('#search-results').append(data).fadeIn(300);
//create_player_scripts();
//set_players();
$('#search-results').find('input[name="cartLink"]').each(function() {
$(this).val($(this).closest('.post1').find('.post_headl a').attr('href'));
});
});
To make an AJAX page reload work I had to first destroy all jplayer instances. So I wrote a little function that grabs all instances of a jplayer on the site (by looking for jp-audio classes) and calls jplayer('destroy'); and jplayer('clearMedia'). This function gets called in the $.ajax({ beforeSend: destroyJplayerInstances(); })
UPDATE:
Here is a statement from the developer of jPlayer, Mark Panaghiston:
https://groups.google.com/forum/#!topic/jplayer/Q_aRhiyYvQo
Hope that helps!

Custom Event For Custom JQuery Plugin

I made this jQuery plugin called removable when you click the objects button it slides up and should trigger a custom event like onDone.
Here's what I did (The codeing format is based on jQuery's http://docs.jquery.com/Plugins/Authoring):
init: function(){
return this.each(function(){
$('a', this).click(function(){
$(this).parent().slideUp(function(){
// Somehow trigger the onDone method
})
});
})
},
onDone: function(){
// Default action
},
and this is what I've done when calling the plugin
$('li').removable({
onDone: function(){
// Overwrite default action
},
})
How can this be done?
If all you need is to call it at the end of the animation, just pass it as the second argument to slideUp or even just call it with $(foo).MyPlugin.onDone() inside the callback function.
otherwise look at trigger and bind jQuery functions - you can use any string you want for those event types so you can trigger and bind a MyPluginDone event
EDIT: based on comments you want something simpler -
As it states in the article you quoted, the best way to provide override-able defaults to options is to have your plugin accept an options object, then to get the combined defaults+overrides you do:
var combinedOpts = $.extend({},defaults,overrides);
and get all the values to use from there...
Try this one.
(function($){
jQuery.fn.extend({
removable: function(options) {
var defaults = {
onDone: function(){alert('default action');}
};
var options = $.extend(defaults, options);
return this.each(function() {
$('a', this).click(function(){
$(this).parent().slideUp(function(){
options.onDone.call();
});
});
});
}
});
})(jQuery);
$('li').removable({
onDone: function(){
alert('Overwrite default action');
},
})

Resources