Extjs MVC nested views events and async function - javascript

I'm developing an extJS 4.2 MVC app.
I've this context menu view object defined:
Ext.define('XApp.view.message.inbox.CtxMenu', {
extend : 'Ext.menu.Menu',
alias : 'widget.inboxctxmenu',
items : [ {
itemId : 'buttonSetFlags',
text : 'ToRead'
}]
});
this context menu is builded when i'm creating this grid (and other my extended grids):
Ext.define('XApp.view.message.inbox.Grid', {
extend: 'Ext.grid.Panel',
alias: 'widget.inboxgrid',
store: 'message.Inbox',
initComponent : function(){
this.menu = this.buildMenu();
this.callParent(arguments);
this.on({
scope : this,
itemcontextmenu : this.onItemContextMenu
});
},
onItemContextMenu : function(grid, record, item, index, e, eOpts) {
console.log('onItemContextMenu');
e.stopEvent();
this.menu.showAt(e.getXY());
},
onDestroy : function(){
console.log('destroy grid and menu');
this.menu.destroy();
this.callParent(arguments);
},
buildMenu : function(){
return Ext.widget('inboxctxmenu');
}
});
this code is extracted from Sencha blog on point 2 to avoid memory leak on nested object.
Now in my controller i want to listen
Ext.define('XApp.controller.Inbox', {
extend : 'Ext.app.Controller',
init : function(application) {
this.control({
"inboxctxmenu #buttonSetFlags" : {
click : this.onFlagsSetter
}
});
},
onFlagsSetter : function(button, e, eOpts) {
this.getController('Message').SetMessageStatus(1,"ToRead",this.getStore('message.Inbox').load);
}
});
in this controller, i call another controller function and i want to reload 'message.Inbox' store:
Ext.define('XApp.controller.Message', {
extend : 'Ext.app.Controller',
SetMessageStatus: function(id,statusToSet,callback) {
Ext.Ajax.request({
url : XApp.util.Util.serverUrl + 'api/message/SetMessageStatus/' + id + "/" + statusToSet,
method : "GET",
failure : function(response, options) {
console.log('Failure' + response);
},
success : function(conn, response, options, eOpts) {
console.log('Success');
if (callback && typeof(callback) === "function") {
console.log('Calling callback');
callback();
}
}
});
}
});
in this function, i've an async call with AJAX, and i want to reload store of InboxController after ajax response, but with this notation, console throw an error.
There are best practices to call async function and launch a callback after success or failure?
Another question is:
what is the best pratices with ExtJs MVC to listen on nested view event (in example my ctxmenu is nested in a grid)? i read for fireevent and bubbleevent but i'm confused...Please bring me back to the right way...

JFYI the context menu in your example is not nested in the grid. Menus are floating objects, and as such they are outside of the usual component hierarchy.
The error you're having is because you're not passing a callback to SetMessageStatus, you're passing the result of expression this.getStore('message.Inbox').load - which evaluates to a function, but without a scope bound to it it's useless. Read this question's answers for more explanations on what the function scope is.
With a naïve head-on approach, the fix would look thusly:
onFlagsSetter: function(button, e) {
var me = this; // Important for the closure below
this.getController('Message').SetMessageStatus(1, 'ToRead', function() {
// Note that within the callback function, `this` is an object
// entirely different from `this` in the above line, so we call
// `getStore` on the captured scope instead.
me.getStore('message.Inbox').load();
});
}
However, a much better approach is to use Controller events:
Ext.define('XApp.controller.Inbox', {
extend: 'Ext.app.Controller',
init: function() {
this.listen({
component: {
'inboxctxmenu #buttonSetFlags': {
click: this.onFlagsSetter
}
},
controller: {
'*': {
statusmessage: this.onStatusMessage
}
}
});
},
onFlagsSetter: function(button) {
this.fireEvent('setstatus', 1, 'ToRead');
},
onStatusMessage: function(success, response) {
if (success) {
this.getStore('message.Inbox').load();
}
}
});
Ext.define('Xapp.controller.Message', {
extend: 'Ext.app.Controller',
init: function() {
this.listen({
controller: {
'*': {
setstatus: this.setMessageStatus
}
}
});
},
setMessageStatus: function(id, statusToSet) {
Ext.Ajax.request({
url: ...,
method: 'GET',
failure: function(response) {
this.fireEvent('statusmessage', false, response);
},
success: function(connection, response) {
this.fireEvent('statusmessage', true, response);
},
// We are setting the above callbacks' scope to `this` here,
// so they would be bound to the Controller instance
scope: this
});
}
});
As you can see, by using Controller events we have decoupled Inbox controller from the Message controller; they are no longer calling each other's methods directly but are passing information instead. The code is much cleaner, and concerns are properly separated.

Related

RequireJS modules are undefined in Backbone view

I have been pretty much beginner at this part of javascript and I would appreciate any ideas how could be solved this problem.
I use requirejs to define my own modules where I also use backbone.js.
Let say I have the main module where I initialize my Backbone view which is rendered without any problem. Also, the click event where is calling method createSchemeForm creates the form correctly. The problem raises up in a situation when I call cancel method by click and the modules which are defined for Backbone view (e.g. "unicorn/sla/dom/helper"...) are undefined but when I called method createSchemeForm at the beginning the modules were executed without any problem.
Thank you in advance for any suggestions.
Backbone view
define("unicorn/sla/view/scheme", [
"unicorn/sla/dom/helper",
"unicorn/soy/utils",
"unicorn/sla/utils"
], function (DOMHelper, soyUtils, jsUtils) {
return Backbone.View.extend({
el: 'body',
inputData: {},
btnSaveScheme: 'btn-save-sla-scheme',
btnCancel: 'btn-cancel-sla-scheme',
btnCreate: 'btn-create-sla-scheme',
btnContainer: '#sla-scheme-buttons-container',
schemeContent: '#sla-scheme-content-section',
btnSpinner: '.button-spinner',
events: {
'click #btn-create-sla-scheme' : "createSchemeForm",
'click #btn-cancel-sla-scheme' : "cancel"
},
initialize: function(){
console.log("The scheme view is initialized...");
this.render();
},
createSchemeForm: function () {
this.spin();
DOMHelper.clearSchemeContent();
DOMHelper.clearButtonsContainer();
//Get button
$btnSave = soyUtils.getButton({isPrimary: 'true', id: this.btnSaveScheme, label: 'Save'});
$btnCancel = soyUtils.getButton({isPrimary: 'false', id: this.btnCancel, label: 'Cancel'});
//Append new created buttons
DOMHelper.addContent(this.btnContainer, AJS.format("{0}{1}", $btnSave, $btnCancel));
//Call service to get entry data for scheme creation form
AJS.$.ajax({
url: AJS.format('{0}={1}',AJS.I18n.getText('rest-url-project-scheme-input-data'), jsUtils.getProjectKey()) ,
type: "post",
async: false,
context: this,
global: false,
}).done(function (data) {
this.inputData = data;
$slaSchemeForm = soyUtils.getSchemeCreateForm({slaScheme : data, helpText: AJS.I18n.getText("sla-time-target-tooltip-text")});
DOMHelper.addContent(this.schemeContent, $slaSchemeForm);
jsUtils.scroll(this.schemeContent, 'slow');
}).fail(function () {
jsUtils.callFlag('error', AJS.I18n.getText("message-title-error"), AJS.I18n.getText("sla-error-load-scheme-input-data"));
}).always(function () {
this.stopSpin();
});
},
spin: function () {
AJS.$('.button-spinner').spin();
},
stopSpin: function () {
AJS.$('.button-spinner').spinStop();
},
cancel: function () {
jsUtils.clearButtonsContainer();
jsUtils.clearSchemeContent();
$btnCreateScheme = soyUtils.getButton({isPrimary: 'false', id: this.btnCreate, label: 'Create SLA Scheme'});
DOMHelper.addContent(this.btnContainer, $btnCreateScheme);
DOMHelper.addContent(this.schemeContent, soyUtils.getSchemesTable(new Array())); // TODO - get current data from server instead of empty array
}
});
});
Main module where is Backbone view initialize
define("unicorn/sla/project/batch", [
"unicorn/sla/utils",
"unicorn/sla/data/operations",
"unicorn/sla/data/validator",
"unicorn/sla/dom/helper",
"unicorn/sla/model/confirm/message",
"unicorn/sla/view/scheme",
"exports"
], function (jsUtils, operations, validator, DOMHelper, ConfirmMessage, SchemeView, exports) {
//Load project batch
exports.onReady = function () {
$schemeView = new SchemeView();
$schemeView.render();
}
});
AJS.$(function () {
AJS.$(document).ready(function () {
require("unicorn/sla/project/batch").onReady();
});
});

How can I prevent Backbones save method from trying to update every model?

I am creating a crud web app with backbone. I am writing the functionality to update a resource (PUT). I am trying to achieve this by fetching a models properties from the server (see the SubscriberView) and on successfully fetching the resource to instantiate a SubscriberEditView whereby the newly fetched model is passed.
So far this works as expected; SubscriberEditView renders an html form which is populated with the model instance properties.
When I enter a new login value into the form I can trigger the update function which successfully makes a PUT request to the server resource and updates the model instance as expected.
However, the problem is that when I then repeat this process with another model instance the PUT request is made against the curent model AND the previously instantiated model.
Is the reason for this because I now have two instances of SubscriberEditView? Or is it something else that I have missed/misunderstood.
Please see below the described code.
// The view for a single subscriber
var SubscriberView = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#subscribers-tmpl').html()),
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
return this;
},
events: {
'click .remove': 'onRemove',
'click .edit-subscriber': 'editSubscriber',
},
editSubscriber: function() {
var getSubscriberModel = this.model.set('id', this.model.attributes.id, {silent:true})
getSubscriberModel.fetch({
success: function (model, response) {
$('#addSubscriber').fadeOut();
new SubscriberEditView({model:model});
},
error: function (response) {
console.log('There was an error');
}
});
},
onRemove: function() {
this.model.destroy();
}
});
// The edit view
var SubscriberEditView = Backbone.View.extend({
tagName: 'div',
el: '#updateSubscriber',
template: _.template($('#subscriberEdit-tmpl').html()),
initialize: function() {
this.model.on('sync', this.render, this);
},
events: {
'click #close': 'cancel',
'click .save-subscriber': 'update'
},
update: function() {
var $login = this.$('#login');
this.model.save({
login: $login.val(),
},
{
dataType: 'text',
success: function (model, response, options) {
console.log('success');
},
error: function (model, response, options) {
console.log('error');
}
});
},
cancel: function() {
$('#addSubscriber').fadeIn();
$('#editInner').fadeOut();
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
},
});
If anyone could help then that would be greatly appreciated.
Cheers
The issue is el: '#updateSubscriber',. All your view instances are pointing to same element to which events are delegated. So clicking on any of the .save-subscriber will trigger update for all the view instances. You should not specify el for a view that is going to have more than one instance.

Firing a custom event in ExtJS 5.1

I have been trying to fire a custom event when a file has been successfully uploaded using a modal window. A grid on the main page listens for the event and should reload its store when a file is successfully uploaded. Problem is, the grid never catches this event.
I think I have a fundamental misunderstanding of how custom events work. What steps should I take to get back on track?
SomeCommonUtilityClass.js
upload: function(args) {
Ext.create('Ext.window.Window', {
/* form with some controls */
buttons: [{
text:'Upload',
handler: function() {
var win = this.up('window');
var form = this.up('form').getForm();
form.submit ({
url: myAjaxCall,
success: function() {
/* fire event here */
win.fireEvent('uploadSuccess');
},
failure: function() {
/*...*/
}
});
}
},
/* etc. */
});
}
SomeOtherFileView.js
{
xtype:'grid',
itemId:'uploadedGrid',
listeners: {
uploadSuccess: 'reloadUploadStore'
},
bind: {
store:'{form}'
},
columns:[/*...*/]
}
SomeOtherFileViewController.js
reloadUploadStore: function() {
console.log("My event fired!") // Never gets here.
/* .... */
store.load({
params: ({
a: "a",
b: "b"
});
callback: function() {
/* do more stuff */
}
});
}
SomeCommonUtilityClass
win.fireEvent('uploadSuccess');
Example of custom event and Controller that listen on it:
SomeOtherFileViewController
init: function() {
this.listen({
// We are using Controller event domain here
controller: {
// This selector matches any originating Controller
'*': {
uploadSuccess: 'reloadUploadStore'
}
}
});
},
reloadUploadStore: function() {
//your code
}
or if you want pass a argument:
win.fireEvent('uploadSuccess',extraArgument);
Controller code is the same. Only your function definition changes:
reloadUploadStore: function(yourArgument) {
//Do your stuff with extraArgument
}

ExtJS5 How to let keymap call viewController function?

I was wondering how to call a controller function from a keymaps in extjs 5?
The following code in the view is working to bind the keys to a an anonymous function, however not to a controller function. The function is also called by the button, and is working. How do I set the scope correctly that it is calling the controller function?
Ext.define("MyApp.view.users.List",{
extend: 'Ext.grid.Panel',
alias: 'widget.usersList',
controller: 'users-list',
listeners: {
scope: 'controller',
afterrender: function(window, options) {
this.keyNav = new Ext.util.KeyMap ({
target:window.el,
binding: [
{key:"s", ctrl:true, fn: function(){alert("hallo shortkey");}},
{key:"c", ctrl:true, fn: "newUserFn"},
],
scope:'controller'
});
}
},
tbar: [
{
itemId: 'users-list-btn-new',
iconCls: 'icon-add',
text: 'New User',
handler: 'newUserFn'
}
]
});
I had similar troubles with scope on key press events. I'm not using a KeyMap as you are but the event logic is still relevant.
The following assumes a controller with method "saveNewComponent" that you want to call. You must fire the view event "callSave" which is being listened to in the view to correctly forward to the controller.
Ext.define('teams.view.component.FormPanel', {
extend: 'Ext.form.Panel',
controller: "FormPanelController",
listeners: {
callSave: "saveNewComponent"
},
beforeShow: function(){
var me = this;
me.body.dom.addEventListener("keydown", Ext.bind(onKeyDown, this), false);
function onKeyDown(e) {
//Listen for Ctrl+S
if (e.ctrlKey && e.which === 83){
e.preventDefault();
me.fireEvent("callSave");
return false;
}
}
},
....
}
I tried many ways of achieving this, this this solution was the only one that had the correct context when actually inside the controller (ie this var was correct)

Backbone.js manually trigger "error" on a collection

I want my collection to fail if the server/json return a specific STATUS (e.g. no results).
The problem: The default error-handler is not called (cause the collection successfully fetches the json. So my idea is use the parse function to look for an error-code in the json.
But how to I trigger the error-method and notify my view (and stop to collection trying to create models)
/*global define*/
define([
'underscore',
'backbone',
'models/mymodel'
], function (_, Backbone, MyModel) {
'use strict';
var SomeCollection = Backbone.Collection.extend({
model: MyModel,
value: null,
url: function() {
return url: "data/list.json";
},
initialize: function(models, options) {
this.zipcode = options.zipcode;
},
parse: function(response, xhr) {
if(response.status == "OK") {
console.info("Status: "+response.status);
return response.results;
} else {
console.warn("Status: "+response.status+" – Message: "+response.message);
this.trigger('fail') // does not work
return response;
}
}
});
return SomeCollection;
});
I have a post on my blog about this kind of things, unfortunately it's in portuguese, but maybe google translate helps you.
http://www.rcarvalhojs.com/dicas/de/backbone/2014/06/24/gerenciando-o-estado-da-aplicacao.html
I like to handle this, in this way
GetSomething:->
#result = #fetch(
success:()=>
#trigger "succesthing1" if #result .status is 204
#trigger "successThing2" if #result .status is 200
error:()=>
#trigger "errorThing" if #result .status is 401
)
Now i can listen for these trigger inside the view and take the correct action for a specific the result from server
There are currently I subscribe for of the Backbone sync, by sending events according to the promise that the request returned, see example below:
(function(Backbone) {
var methods, _sync;
_sync = Backbone.sync;
methods = {
beforeSend: function() {
return this.trigger("sync:start", this);
},
error: function() {
return this.trigger("sync:error", this);
},
complete: function() {
return this.trigger("sync:stop", this);
}
};
Backbone.sync = function(method, entity, options) {
var sync;
if (options == null) {
options = {};
}
_.defaults(options, {
beforeSend: _.bind(methods.beforeSend, entity),
error: _.bind(methods.error, entity)
complete: _.bind(methods.complete, entity)
});
sync = _sync(method, entity, options);
if (!entity._fetch && method === "read") {
return entity._fetch = sync;
}
};
})(Backbone);
Hope this helps.

Categories