I have a grid, the rows of which can be clicked on. The click fires an event that is then captured by the controller. Is there a way for that controller to open a popup and call a controller to populate that popup with its associated view? This is what I have in my grid's controller now:
init: function() {
...
this.control({
'shipmentsgrid': {
itemrowclick: this.itemRowClick
}
})
},
itemRowClick: function() {
var win = new Ext.Window({var win = new Ext.Window({
items: [{
xtype: 'shipmentmonthly' // This is not recognized
}]
}).show();
}
I am not quite sure what you trying to archive. But you can easily fetch a instance of another controller using getController('YourControllerName') called from any controller scope. That will present you with a instance of this controller (and even load necessary classes). Now you are free to call any method on this controller with any arguments. For example you may also either provide the instance of this controller as argument and us that one as scope with or use this (But that depends on your implementation)
For your example:
itemRowClick: function() {
var ctrl = this.getController('Controller2');
var win = ctrl.openWin();
win.show();
}
// resident in controller 2
openWin: function() {
var win = Ext.create('Ext.window.Window',{
items: [{
xtype: 'shipmentmonthly' // This is not recognized
}]
});
return win;
}
Related
In controller
listen: {
global: {
'ontest': 'ontestfunction'
}
},
ontestfunction: function(){
alert('oks 31');
}
In view
listeners: {
element: 'element',
click: function(){
Ext.GlobalEvents.fireEvent('ontest');
}
}
It is the only way I've found to work, you know some other way?
You can get any controller using Ext.app.Controller.getController.
Since application is derived from controller, all you have to know is the name of your app and the desired controller; and calling the function is a piece of cake:
var configController = MyAppName.app.getController("MyAppName.controller.Configuration");
configController.ontestfunction();
Since you're using EXT 6, I assume you're using viewcontrollers and not controllers.
Ext is going to resolve the scope as the view's controller automatically, no need to write any extra code.
First make sure you define the viewcontroller as the controller of the view:
controller: 'aliasofviewcontroller'
after that you can basically assign any of the controllers functions to the handlers of the components on the view.
Let's say you have the following function in your controller:
onClickCustomHandler:function(e){
...
}
Using the following syntax, the function is going to get called every time you're button is clicked:
Ext.create('Ext.Button', {
text: 'My button',
handler: 'onClickCustomHandler'
});
Or using xtype:
{
xtype:'button',
text:'My button',
handler: 'onClickCustomHandler'
}
For further reading: http://docs.sencha.com/extjs/6.0.2/guides/application_architecture/view_controllers.html
The following code in the $timeout is never called. I can put any wrong test I like in there and the test always passes (there is a similar question (Karma e2e testing: how to know when the DOM is ready?) but it does not provide a solution):
it('should display a filter row when attribute sg-live-filtering is present', function()
{
angular.mock.inject(function($compile, $rootScope, $timeout) {
var elem = $compile('<div sg-grid sg-data="api/grid/accounts" sg-live-filtering></div>')(scope); // the angular-kendo grid
$rootScope.$apply();
var table = elem.find('table[role="grid"]'); // find the kendo grid
expect(table.length).toBe(1);
var header = table.find('thead'); // find the grid's table header
expect(header.length).toBe(1);
$timeout(function () {
// get the second row in the header and check it has a specific class
expect(header.find('tr').eq(1).hasClass('sg-grid-filter-row')).toBeTruthy();
// e.g. I could do this and it would still pass!!!
expect(header.find('tr').eq(500));
});
}
}
PhantomJS 1.9.2 (Windows 7): Executed 1 of 871 (skipped 383) SUCCESS (0 secs / 0
This is what it looks like in the browser:
The kendo grid is created using a standard angularjs directive:
angular.module('sgComponents').directive('sgGrid', [
templateUrl: 'sg-grid.html',
// lots of kendo code follows to build the grid
]);
The external sg-grid.html template:
<div sg-grid sg-data="api/grid/accounts"
sg-live-filtering> <!-- the attribute I want to test -->
</div>
When the directive code runs, there is a check to see if the sg-live-filtering attr is present. If it is, a utility function is called to append the row you see highlighted in the image to the grid's table header:
if (attrs.sgLiveFiltering) {
/*
timeout is needed to ensure DOM is ready. COULD THIS BE THE PROBLEM?
*/
$timeout(function () {
/*
this function adds the filter row to the grid.
THE NEW ROW HAS CLASS 'sg-grid-filter-row' THAT I USE FOR TESTING
*/
buildGridFilterRow(elm, scope, attrs);
});
}
Can you display your test code ???
Because you have to wait in order to execute timeout or just use $timeout.flush();
Here is an example:
Unit testing an asynchronous service in angularjs
Finally (2 weeks later!) got this to work. #idursun and igorzg were correct when they suggested using $timeout.flush(100) but it also needed, as #idursun further mentioned in his comments, that the $timeout block be removed too.
When I initially tried this, it didn't work but now it does. Don't ask me why, all I care is that it is working! Here's my full test case for reference:
it('should display a filter row when attribute sg-live-filtering is present', function () {
angular.mock.inject(function($compile, $rootScope, $timeout) {
var scope = $rootScope.$new();
scope.accountColumnsForAdvSearch = [
{field: 'accountId', title: 'AccountId', dataType: 'string'},
{field: 'name', title: 'Account Name', dataType: 'string'},
{field: 'shortName', title: 'Short Name', dataType: 'string'},
{field: 'status', title: 'Status', dataType: 'string'}
];
$httpBackend.when('GET', 'api/grid/accounts').respond(accountData);
var elem = $compile('<div sg-grid sg-data="api/grid/accounts" sg-columns="accountColumnsForAdvSearch" sg-live-filtering="true"></div>')(scope);
$rootScope.$apply();
$httpBackend.flush();
$timeout.flush(100); // wait for DOM to load
var table = elem.find('table[role="grid"]'); // the kendo grid
expect(table.length).toBe(1);
var filterRow = table.find('.sg-grid-filter-row'); // the filter row
expect(filterRow.length).toBe(1);
});
I'm new to extjs and I'm using the MVC architecture.
When my application references a method of a controller, I do it that way (in MyApp.Application):
Mb.app.getController('Main').myMethod();
It is already long, but I think this is the way to do.
When a controller calls it's own method in a closure, I was led to use this code (in MyApp.controller.Main:
controllerMethodOne: function(){
Ext.Ajax.request({
url: ...,
params: ...,
success: (function(response){
list = Ext.JSON.decode(response.responseText);
list.forEach(function(item){
storeMenu.add(
Ext.create('Ext.menu.Item', {
text: item.text,
handler: function(el){MyApp.app.getController('Main').controllerMethodTwo()}
})
)
})
})
})
},
I referenced the method with MyApp.app.getController('Main').controllerMethodTwo() because this is not refering to the controller object in the closure, and thus this..controllerMethodTwo()isn't working.
I find this utterly convoluted, and I hope someone has an idea to get around that MyApp.app.getController-workaround.
Update
Thanks to all the suggestion I could optimize my code and came up with:
// in my controller
mixins: ['Mb.controller.mixin.StoreMenu'],
// I use that style of menus in two controllers thats why I use a mixin
init: function() {
this.control({
'#vg_storeMenu menuitem': {
click: this.onStoreMenuClicked
}
})
},
// the controller mixin
Ext.define('Mb.controller.mixin.StoreMenu', {
extend: 'Ext.app.Controller',
buildStoreMenu: function(store_name){
var storeMenu = Ext.ComponentQuery.query('#' + store_name + 'Menu')[0];
Ext.Ajax.request({
url: Paths.ajax + 'json.php',
params: {list: store_name + 's'},
success: (function(response){
list = Ext.JSON.decode(response.responseText);
items = Ext.Array.map(list, function(item) {
return {
xtype: 'menuitem',
text: item.text
}
});
storeMenu.add(items);
})
})
},
onStoreMenuClicked: function(el){
...
}
});
Actually, there are at least four distinctly different problems in your code:
Scope handling for intra-class method calls
Component creation inefficiency
Component event handling in a controller
Inter-controller communication
Scope handling
The first one is solved either by using a closure, or passing in the scope parameter to Ajax request, as #kevhender described above. Given that, I'd advocate writing clearer code:
controllerMethodOne: function() {
Ext.Ajax.request({
url: ...,
params: ...,
scope: this,
success: this.onMethodOneSuccess,
failure: this.onMethodOneFailure
});
},
// `this` scope is the controller here
onMethodOneSuccess: function(response) {
...
},
// Same scope here, the controller itself
onMethodOneFailure: function(response) {
...
}
Component creation
The way you create menu items is less than efficient, because every menu item will be created and rendered to the DOM one by one. This is hardly necessary, either: you have the list of items upfront and you're in control, so let's keep the code nice and declarative, as well as create all the menu items in one go:
// I'd advocate being a bit defensive here and not trust the input
// Also, I don't see the `list` var declaration in your code,
// do you really want to make it a global?
var list, items;
list = Ext.JSON.decode(response.responseText);
items = Ext.Array.map(list, function(item) {
return {
xtype: 'menuitem',
text: item.text
}
});
// Another global? Take a look at the refs section in Controllers doc
storeMenu.add(items);
What changes here is that we're iterating over the list and creating a new array of the soon-to-be menu item declarations. Then we add them all in one go, saving a lot of resources on re-rendering and re-laying out your storeMenu.
Component even handling
It is completely unnecessary, as well as inefficient, to set a handler function on every menu item, when all this function does is call the controller. When a menu item is clicked, it fires a click event - all you need to do is to wire up your controller to listen to these events:
// Suppose that your storeMenu was created like this
storeMenu = new Ext.menu.Menu({
itemId: 'storeMenu',
...
});
// Controller's init() method will provide the wiring
Ext.define('MyController', {
extend: 'Ext.app.Controller',
init: function() {
this.control({
// This ComponentQuery selector will match menu items
// that descend (belong) to a component with itemId 'storeMenu'
'#storeMenu menuitem': {
click: this.controllerMethodTwo
}
});
},
// The scope is automatically set to the controller itself
controllerMethodTwo: function(item) {
...
}
});
One best practice is to write the ComponentQuery selectors as finely grained as feasible, because they're global and if you're not precise enough your controller method may catch events from unwanted components.
Inter-controller communication
This is probably a bit far fetched at the moment, but since you're using Ext JS 4.2 you may as well take advantage of the improvements we've added in that regard. Before 4.2, there was a preferred (and only) approach to call one controller's methods from another controller:
Ext.define('My.controller.Foo', {
extend: 'Ext.app.Controller',
methodFoo: function() {
// Need to call controller Bar here, what do we do?
this.getController('Bar').methodBar();
}
});
Ext.define('My.controller.Bar', {
extend: 'Ext.app.Controller',
methodBar: function() {
// This method is called directly by Foo
}
});
In Ext JS 4.2, we've added the concept of event domains. What it means is that now controllers can listen not only to component's events but to other entities events, too. Including their own controller domain:
Ext.define('My.controller.Foo', {
extend: 'Ext.app.Controller',
methodFoo: function() {
// Effectively the same thing as above,
// but no direct method calling now
this.fireEvent('controllerBarMethodBar');
}
});
Ext.define('My.controller.Bar', {
extend: 'Ext.app.Controller',
// Need some wiring
init: function() {
this.listen({
controller: {
'*': {
controllerBarMethodBar: this.methodBar
}
}
});
},
methodBar: function() {
// This method is called *indirectly*
}
});
This may look like a more convoluted way to do things, but in fact it's a lot simpler to use in large(ish) apps, and it solves the main problem we've had: there is no need for hard binding between controllers anymore, and you can test each and every controller in isolation from others.
See more in my blog post: Controller events in Ext JS 4.2
this doesn't work in the success callback because it doesn't have the right scope. Your 2 options are to:
1: Create a variable at the beginning of the function to reference in the callback:
controllerMethodOne: function(){
var me = this;
Ext.Ajax.request({
url: ...,
params: ...,
success: (function(response){
list = Ext.JSON.decode(response.responseText);
list.forEach(function(item){
storeMenu.add(
Ext.create('Ext.menu.Item', {
text: item.text,
handler: function(el){me.controllerMethodTwo()}
})
)
})
})
})
},
2: Use the scope config of the Ext.Ajax.request call:
controllerMethodOne: function(){
Ext.Ajax.request({
url: ...,
params: ...,
scope: this,
success: (function(response){
list = Ext.JSON.decode(response.responseText);
list.forEach(function(item){
storeMenu.add(
Ext.create('Ext.menu.Item', {
text: item.text,
handler: function(el){me.controllerMethodTwo()}
})
)
})
})
})
},
I am exploring the BBCloneMail demo application for MarionetteJS, but I am not seeing how the events are triggering the rendering actions. I saw some global 'show' event here:
https://github.com/marionettejs/bbclonemail/blob/master/public/javascripts/bbclonemail/components/appController.js#L25
show: function(){
this._showAppSelector("mail");
Marionette.triggerMethod.call(this, "show");
},
But I don't see, where/how the Marionette.triggerMethod results into rendering the Mail component. I was trying to call the triggerMethod for my case, but I get a 'cannot call apply for undefined'. Why is the call above working for the BBcloneMail application.
The Application controller for my case:
MA.AppController = Marionette.Controller.extend({
initialize: function(){
_.bindAll(this, "_showGenres");
},
show: function() {
if (MA.currentUser) {
MA.navbar.show(new MA.Views.Items.LogoutNavbar({model: MA.currentUser}));
}
else
{
MA.navbar.show(new MA.Views.Items.LoginNavbar());
}
this._showGenres();
},
_showGenres: function() {
var categoryNav = new MA.Navigation.Filter({
region: MA.filter
});
this.listenTo(categoryNav, "genre:selected", this._categorySelected);
categoryNav.show();
MA.main.show(MA.composites.movies);
},
showMovieByGenre: function(genre){
var movies = new MA.Controllers.MoviesLib();
that = this;
$.when(movies.getByCategory(genre)).then(that._showMovieList);
Backbone.history.navigate("#movies/genres/" + genre);
},
_showMovieList: function(movieList){
var moviesLib = new MA.Controllers.MoviesLib({
region: MA.main,
movies: movieList
});
Marionette.triggerMethod.call(this, "show");
}
});
I init the application controller in a init.js with:
app = new MA.AppController();
Looking at the source for triggerMethod, this is a way of both triggering an event (the string being passed in), and additionally (if it exists) running a method on the object that has an 'on' prefix.
In your case the error relates to line 560, specifically that there is no method apply on undefined. Based on the code its (in your case) trying to call the equivilent of this.trigger('show') - but AppController doesn't have a method called trigger.
In which case I'm guessing that in the BBCloneMail example this (being bassed into triggerMethod.call) is not actually the controller, but instead the view that is to be shown.
I have a Grid Panel with a toolbar and an context menu.
The toolbar has a edit button and the context menu has a edit menu item.
Both shares the same properties (text, icon and handler)
Ext has something called Action which makes it possible to share functionality etc. between components, but til now I have had no success getting it to work in the MVC architecture
(I am using the new MVC architecture in 4.0)
My Action class looks like this:
Ext.define( 'App.action.EditAction', {
extend: 'Ext.Action',
text: 'Edit',
handler: function()
{
Ext.Msg.alert('Click', 'You did something.');
},
iconCls: 'icon-edit-user' ,
});
And in my context menu
requires: ['App.action.EditAction'],
initComponent: function()
{
var editUser = new App.action.EditAction();
this.items = [
editUser,
{
// More menuitems
}
...
];
this.callParent(arguments);
When running the code I get "config is undefined" in the console.
Can anyone point out what I am doing wrong?
Thanks in advance,
t
Passing an empty config to your constructor will avoid the error, but have unwanted consequences later because, unfortunately, the base class (Ext.Action) relies on this.initialConfig later on. For example, if you called editUser.getText() it would return undefined instead of the expected 'Edit'.
Another approach is to override your constructor to allow a no-arg invocation and apply your overridden configuration:
Ext.define( 'App.action.EditAction', {
extend: 'Ext.Action',
text: 'Edit',
constructor: function(config)
{
config = Ext.applyIf(config || {}, this);
this.callParent([config]);
},
handler: function()
{
Ext.Msg.alert('Click', 'You did something.');
},
iconCls: 'icon-edit-user' ,
});
As per Ext.Action constructor
constructor : function(config){
this.initialConfig = config;
this.itemId = config.itemId = (config.itemId || config.id || Ext.id());
this.items = [];
}
You must supply config not to get config is undefined exception in the second line (precisely in config.itemId part).
Updating your code as var editUser = new App.action.EditAction({}); should help(passing new empty object as config).
Surely, you could add some properties to the config object too.