App structure and App.vent.bind - marionette

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.

Related

Pass DataTable reference to the callback function on load

My current code is:
var CommissionLogs = $("#CommissionLogs").DataTable({
ajax: {
url: ajaxurl + '?action=pos&post_action=get_commissions'
},
'initComplete': function (settings, json){
//possible to access 'this'
this.api().columns(1);
}
});
I improved the code above as below with help :
var CommissionLogs = $("#CommissionLogs").DataTable({
ajax: {
url: ajaxurl + '?action=pos&post_action=get_commissions'
},
'initComplete': function(settings, json){
callbackFunction(settings);
}
});
function callbackFunction(settings){
var api = new $.fn.dataTable.Api( settings );
// api is accessible here.
}
Update :
Now I can access api from callback function. But I want use same callback with load() as below code.
CommissionLogs.ajax.url( newAjaxURL ).load( callbackFunction(), true);
But settings param is not accessible in load function.
I can clear and destroy datatable and re initialize always. But what will be the right way.
I think you need settings:
https://datatables.net/reference/type/DataTables.Settings
$('#example').dataTable( {
"initComplete": function(settings, json) {
myFunction(settings);
}
});
function myFunction(settings){
var api = new $.fn.dataTable.Api( settings );
// Output the data for the visible rows to the browser's console
// You might do something more useful with it!
console.log( api.rows( {page:'current'} ).data() );
}
Other option is re-use your var CommissionLogs variable throughout the code without using this, I recommend strongly this last option.
The dataTable.ajax.url().load() has not access to settings.
So can not call a callback function with settings.
But possible to use callback function without settings.
So here is an alternative way to use settings.
CommissionLogs.clear();// clear the table
CommissionLogs.destroy();// destroy the table
CommissionLogs = $("#CommissionLogs").DataTable({
ajax: {
url: newAjaxUrl
},
'initComplete': function (settings, json){
callbackDatatableFunciton(settings);
}
});

How to ajax call from RiotControl Store?

I have created APIs with Nodejs/Express.
Let say I can do GET request to localhost:8080/list and it returns JSON of my TODO list and I can POST to localhost:8080/list to create new to do list.
Then I use Riotjs + Riotcontrol for my Frontend website.
How do I request from todostore.js file?
This is the riotcontrol todostore.js file which I get from riotcontrol demo folder
Riotcontrol
// TodoStore definition.
// Flux stores house application logic and state that relate to a specific domain.
// In this case, a list of todo items.
function TodoStore() {
riot.observable(this) // Riot provides our event emitter.
var self = this
self.todos = [
{ title: 'Task 1', done: false },
{ title: 'Task 2', done: false }
]
// Our store's event handlers / API.
// This is where we would use AJAX calls to interface with the server.
// Any number of views can emit actions/events without knowing the specifics of the back-end.
// This store can easily be swapped for another, while the view components remain untouched.
self.on('todo_add', function(newTodo) {
self.todos.push(newTodo)
self.trigger('todos_changed', self.todos)
})
self.on('todo_remove', function() {
self.todos.pop()
self.trigger('todos_changed', self.todos)
})
self.on('todo_init', function() {
self.trigger('todos_changed', self.todos)
})
// The store emits change events to any listening views, so that they may react and redraw themselves.
}
You could do something like this in your TodoStore
self.on('todo_init', function() {
// Trigger loading here perhaps, then set loading = false when it's loaded
//self.trigger('set_loading', {value: true})
fetch('http://localhost:8080/list')
.then(response => response.json())
.then(function (json) {
self.todos = json
self.trigger('todos_changed', self.todos)
})
})

How to deal with async requests in Marionette?

I am trying to fill in an ItemView in Marionette with the combined results of 2 API requests.
this.standings = App.request('collection:currentStandings');
this.userInfo = App.request('model:userInfo');
this.standings.each(function(s) {
if (s.currentUser) {
s.set('alias', this.userInfo.alias);
s.set('imageURL', this.userInfo.imageURL);
}
});
userInfoView = new LeagueBar.UserInfo({ collection: this.standings });
The problem is, the combination never happens because the requests have not been fulfilled before I try to combine them.
I know I probably need to add a promise for each request, but I haven't been able to find a clean way to do it. I could make 'collection:currentStandings' and 'model:userInfo' return promises, however, they are currently used in many other parts of the code, so I would have to go back and add .then()s and .done()s all over the code base where they weren't required before.
Any ideas or suggestions?
EDIT:
I have currently solved this in a less-than-ideal way: I created a template/view for the alias and a template/view for the imageURL and kept the template/view for the standings info. This doesn't seem like the best way and I'm interested to know the right way to solve this problem.
here are the two requests I am trying to combine:
Models.CurrentStandings = App.Collection.extend({
model: Models.PlayerStandings,
url: function() { return 'leagues/' + App.state.currentLeague + '/standings'; },
parse: function(standings) {
return _.map(standings, function(s) {
if (s.memberId == App.user.id)
s.currentUser = true;
return s;
});
}
});
App.reqres.setHandler('collection:currentStandings', function() {
weekStandings = new Models.CurrentStandings();
weekStandings.fetch({ success: function(data){ console.log(data); }});
return weekStandings;
});
Models.UserInfo = App.Model.extend({
url: 'users/me'
});
App.reqres.setHandler('model:userInfo', function(options) {
myuser = new Models.UserInfo();
myuser.fetch(options);
return myuser;
});
There are 2 solutions which based on your dependencies among views can be selected:
You can create views which are handling 'change' event of Models.UserInfo and when the data is ready (Change/Reset event raised) re-render the content. It is probably your solution.
If you are looking for a solution which should not create instance of LeageBar.UserInfo until both Models.CurrentStanding and Models.UserInfo are ready, you have to return the result of fetch function, so you may remove calling fetch from setHandlers and use them as following:
this.standings = App.request('collection:currentStandings');
this.userInfo = App.request('model:userInfo');
var that=this;
that.standings.fetch().done(function(){
that.userInfo.fetch().done(function(){
that.standings.each(function(s) {
if (s.currentUser) {
//....
}
});
userInfoView = new LeagueBar.UserInfo({ collection: that.standings });
});

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

Resources