This may be a duplicate question but in either case I wanted to ask.
I am a beginner ExtJS 4 developer and I am learning ExtJS using Loiane Groner's book, Mastering ExtJS 4. So far so good, but when I got to use refs the app breaks telling me that the autogenerated method is unavailable:
Here is my Login controller code:
Ext.define('Packt.controller.Login', {
extend: 'Ext.app.Controller',
requires:[
'Packt.util.MD5'
],
views:[
'Login',
'authentication.CapsLockTooltip'
],
refs: {
ref: 'capslocktooltip',
selector: 'capslocktooltip',
autoCreate : true
},
init: function(){
this.control({
"login form button#submit":{
click: this.onButtonClickSubmit
},
"login form button#cancel": {
click: this.onButtonClickCancel
},
"login form textfield": {
specialkey:this.onTextfieldSpecialKey
},
"login form textfield[name=password]": {
keypress: this.onTextfieldKeyPress
}
});
},
onTextfieldKeyPress: function(field, e, options){
var charCode = e.getCharCode();
if((e.shiftKey && charCode >= 97 && charCode <= 122) ||
(!e.shifKey && charCode >= 65 && charCode <= 90)){
if(this.getCapsLockTooltip() === undefined) {
Ext.widget('capslocktooltip');
}
} else {
if(this.getCapsLockTooltip() !== undefined) {
this.getCapsLockTooltip().hide();
}
}
},
onTextfieldSpecialKey: function(field, e, options){
if(e.getKey() == e.ENTER){
var submitBtn = field.up('form').down('button#submit');
submitBtn.fireEvent('click', submitBtn, e, options);
}
},
onButtonClickSubmit: function(button, e, options){
console.log('login submit');
var formPanel = button.up('form'),
login = button.up('login'),
user = formPanel.down('textfield[name=user]').getValue(),
pass = formPanel.down('textfield[name=password]').getValue();
if (formPanel.getForm().isValid()){
Ext.get(login.getEl()).mask("Authenticating... Please wait...", 'loading');
pass = Packt.util.MD5.encode(pass);
Ext.Ajax.request({
url:'php/login.php',
params:{
user:user,
password:pass
},
success: function(conn, response, options, eOpts){
Ext.get(login.getEl()).unmask();
var result = Ext.JSON.decode(conn.responseText, true);
if(!result){
result = {};
result.success = false;
result.msg = conn.responseText;
}
if(result.success){
login.close();
Ext.create('Packt.view.MyViewport');
} else {
Ext.Msg.show({
title:'Fail!',
msg: result.msg,
icon:Ext.Msg.ERROR,
buttons: Ext.Msg.OK
});
}
},
failure: function(conn, response, options, eOpts){
Ext.get(login.getEl()).unmask();
Ext.Msg.show({
title: 'Error!',
msg: conn.responseText,
icon: Ext.Msg.ERROR,
button: Ext.Msg.OK
});
}
});
}
},
onButtonClickCancel: function(button, e, options){
console.log('login cancel');
button.up('form').getForm().reset();
}
});
In firebug is see this:
TypeError: this.getCapsLockTooltip is not a function
I also was checking the Ext object inside Firebug and the closest thing to my function was this:
Ext.app.Application.instance.getController('Login').getAuthenticationCapsLockTooltipView();
But i didn't find the required function. What do I do wrong?
I follow the book and the above code is what you get.
Here is the caps lock view:
Ext.define('Packt.view.authentication.CapsLockTooltip', {
extend: 'Ext.tip.QuickTip',
alias: 'widget.capslocktooltip',
target: 'password',
anchor: 'top',
anchorOffset: 60,
width: 300,
dismissDelay: 0,
autoHide: false,
title: '<div class="capslock">Caps Lock is On</div>',
html:'<div>Having caps log on may cause you the enter password incorrectly.</div>'
});
The ref is case sensitive so the function what is created is getCapslocktooltip
When using refs see also Blessing and Curse of refs article
I found in the ExtJS 4 docs that refs is and array so when using it don't forget to add square brackets lik this:
refs:[
{
ref: 'capsLockTooltip',
selector: 'capslocktooltip'
}
]
http://docs.sencha.com/extjs/4.2.0/#!/api/Ext.app.Controller-cfg-refs
So now when you search JS memory with
Ext.app.Application.getController('Login').getCapsLockTooltip();
getCapsLockTooltip() function will exist. Also selector would be the alias name of the components you are trying to access.
Also just to note, Mastering ExtJS 4 by Loiane Groner has code errors.
Related
I need to employ a filter function to implement a heuristic for selecting records. Simple field/value checks, alone, are inadequate for our purpose.
I'm trying to follow the examples for function filters, but for some reason, the "allowFunctions" flag keeps getting set to false.
I attempt to set the allowFunctions property to true in the storeConfig:
storeConfig: {
models: ['userstory', 'defect'],
allowFunctions: true,
filters: [{
// This did not work ...
property: 'Iteration.Name',
value: 'Sprint 3',
// Trying dynamic Filter Function. Update: Never called.
filterFn: function (item) {
console.log("Entered Filter Function!");
var iter = item.get("Iteration");
console.log("Iteration field: ", iter);
if (iter !== null && iter !== undefined) {
return (iter.name === "Sprint 3");
} else {
return false;
}
}
}]
},
After the grid view renders, I inspect it the store configuration and its filters:
listeners: {
afterrender: {
fn: function (_myVar, eOpts) {
console.log("Arg to afterrender: ", _myVar, " and ", eOpts);
var _myStore = _myVar.getStore();
console.log("Store filters: ", _myStore.filters);
}
}
},
What I find is that the allowFunctions property has been set back to false and I see that the filter function I specified never fired.
Console Screen Shot
So either I am setting allowFunctions to true in the wrong place, or something built into the Rally Grid View and its data store prohibits filter functions and flips the flag back to false.
OR there's a third option betraying how badly off my theory of operation is.
Oh, wise veterans, please advise.
Here's the entire Apps.js file:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function () {
//Write app code here
console.log("Overall App Launch function entered");
//API Docs: https://help.rallydev.com/apps/2.1/doc/
}
});
Rally.onReady(function () {
Ext.define('BOA.AdoptedWork.MultiArtifactGrid', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function () {
console.log("onReady Launch function entered");
this.theGrid = {
xtype: 'rallygrid',
showPagingToolbar: true,
showRowActionsColumn: false,
editable: false,
columnCfgs: [
'FormattedID',
'Name',
'ScheduleState',
'Iteration',
'Release',
'PlanEstimate',
'TaskEstimateTotal',
'TaskActualTotal', // For some reason this does not display ?? :o( ??
'TaskRemainingTotal'
],
listeners: {
afterrender: {
fn: function (_myVar, eOpts) {
console.log("Arg to afterrender: ", _myVar, " and ", eOpts);
var _myStore = _myVar.getStore();
console.log("Store filters: ", _myStore.filters);
}
}
},
storeConfig: {
models: ['userstory', 'defect'],
allowFunctions: true,
filters: [{
// This did not work ...
property: 'Iteration.Name',
value: 'Sprint 3',
// Trying dynamic Filter Function. Update: Never called.
filterFn: function (item) {
console.log("Entered Filter Function!");
var iter = item.get("Iteration");
console.log("Iteration field: ", iter);
if (iter !== null && iter !== undefined) {
return (iter.name === "Sprint 3");
} else {
return false;
}
}
}]
},
context: this.getContext(),
scope: this
};
this.add(this.theGrid);
console.log("The Grid Object: ", this.theGrid);
}
});
Rally.launchApp('BOA.AdoptedWork.MultiArtifactGrid', {
name: 'Multi-type Grid'
});
});
This is a tricky one since you still want your server filter to apply and then you want to further filter the data down on the client side.
Check out this example here:
https://github.com/RallyCommunity/CustomChart/blob/master/Settings.js#L98
I think you can basically add a load listener to your store and then within that handler you can do a filterBy to further filter your results on the client side.
listeners: {
load: function(store) {
store.filterBy(function(record) {
//return true to include record in store data
});
}
}
I'm not familiar with allowFunctions, but in general remoteFilter: true/false is what controls whether the filtering is occurring server side or client side. remoteFilter: true + the load handler above gives you the best of both worlds.
I need to apply some computed filtering to the data store associated with a Rally Grid.
This code has a good bit of debugging "noise," but it shows that I'm trying to provide some filters at config time, and they're ignored, or seem to be since my filter function is not firing.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function () {
//Write app code here
console.log("Overall App Launch function entered");
//API Docs: https://help.rallydev.com/apps/2.1/doc/
}
});
Rally.onReady(function () {
Ext.define('BOA.AdoptedWork.MultiArtifactGrid', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function () {
console.log("onReady Launch function entered");
this.theGrid = {
xtype: 'rallygrid',
showPagingToolbar: true,
showRowActionsColumn: false,
editable: false,
columnCfgs: [
'FormattedID',
'Name',
'ScheduleState',
'Iteration',
'Release',
'PlanEstimate',
'TaskEstimateTotal',
'TaskActualTotal', // For some reason this does not display ?? :o( ??
'TaskRemainingTotal'
],
listeners: {
afterrender: {
fn: function (_myVar, eOpts) {
console.log("Arg to afterrender: ", _myVar, " and ", eOpts);
console.log("Filters: ", _myVar.filters);
var _myStore = _myVar.getStore();
console.log("Store : ", _myStore);
console.log("Store filters: ", _myStore.filters);
}
}
},
filters: [{
// This did not work ...
property: 'ScheduleState',
operator: '==',
value: 'Defined',
// Trying dynamic Filter Function. Update: Never called.
filterFn: function (item) {
console.log("Entered Filter Function!");
var iter = item.get("Iteration");
console.log("Iteration field: ", iter);
if (iter !== null && iter !== undefined) {
return (iter.name === "Sprint 3");
} else {
return false;
}
}
}],
context: this.getContext(),
storeConfig: {
models: ['userstory', 'defect']
},
scope: this
};
this.add(this.theGrid);
console.log("The Grid Object: ", this.theGrid);
}
});
Rally.launchApp('BOA.AdoptedWork.MultiArtifactGrid', {
name: 'Multi-type Grid'
});
});
I have not coded in 12 years and never before in JavaScript. So, I'm getting my bearings.
Someone in the Rally Communities provided the answer and helpful feedback:
corkr03 said ...
#miguelfuerte a few things:
The "filters" configuration needs to be part of the storeConfig. In your code above it is part of the gridConfig.
storeConfig: {
filters: [{
property: "Iteration.Name",
value: "Sprint 3"
}]
}
Also, the filter for a property of "Iteration" will expect a reference to the Iteration reference. For that particular implementation, you will want to use the property: "Iteration.Name". There is good information about queries and using dot notation here: General Query Examples | CA Agile Central Help
I need to add a button to the taskbar quickstart, but i do not want to open a module window, for example a logout button that will show a confirm messagebox, i have tried like this:
getTaskbarConfig: function () {
var ret = this.callParent();
me = this;
return Ext.apply(ret, {
quickStart: [
{ name: 'Window', iconCls: 'icon-window', module: 'ext-win' },
{ name: 'Logout', iconCls:'logout', handler: me.onLogout}
]
});
},
onLogout: function () {
Ext.Msg.confirm('Logout', 'Are you sure you want to logout?');
},
And i changed the getQuickStart function of the TaskBar.js file to this:
getQuickStart: function () {
var me = this, ret = {
minWidth: 20,
width: Ext.themeName === 'neptune' ? 70 : 60,
items: [],
enableOverflow: true
};
Ext.each(this.quickStart, function (item) {
ret.items.push({
tooltip: { text: item.name, align: 'bl-tl' },
overflowText: item.name,
iconCls: item.iconCls,
module: item.module,
//handler: me.onQuickStartClick, **original code**
handler: item.handler == undefined ? me.onQuickStartClick : item.handler,
scope: me
});
});
return ret;
}
But it does not work, is there a way to add a simple button to the taskbar quickstart?
Thanks for your reply. I have solved the issue. In the TaskBar.js file i changed this line:
handler: item.handler == undefined ? me.onQuickStartClick : item.handler
for this one:
handler: item.handler ? item.handler : me.onQuickStartClick
Actually, for me, both do the same, but for any weird reason the code works with that change.
this.store = Ext.create('Ext.data.Store', {
fields: [
'id',
'name',
'Address',
'status',
],
autoLoad: auto,
autoSync: auto,
remoteSort: true,
proxy: {
type: 'ajax',
api: {
create: '../../create.php',
read: '../../read.php',
destroy: '../../destroy.php',
update: '../../update.php'
},
reader: {
type: 'json',
root: '__data',
totalProperty: 'grandTotal'
},
writer: {
type: 'json',
root: '__data'
},
listeners: {
exception: function( t, response, op ) {
var _da = Ext.decode( response.responseText );
if( _da ) {
if( _da.message == "ExistingName" ) {
_da.message = Locale.gettext('name already exists');
} else {
frm = _self.subnetEditor.down('form');
name_field = frm.down('textfield[name=name]');
}
name_field.markInvalid(Locale.gettext(_da.message));
}
showMsg( _da.success, _da.message );
if( op.action == 'create' || op.action == 'update' ) {
_self.store.rejectChanges();
_self.store.load();
}
}
}
}
}
});
This is the store that calls four php files to do the CRUD, and some listener to process the duplicate name.
removeSelected: function() {
var _self = this;
Ext.Msg.show( {
title: Locale.gettext( 'Remove selected?' ),
msg: Locale.gettext( 'Are you sure you want to remove ALL SELECTED items?' ),
icon: Ext.Msg.WARNING,
buttons: Ext.Msg.OKCANCEL,
buttonAlign: 'right',
fn: function( button ) {
if( button == 'ok' ) {
var grid = _self.down( 'grid' );
if( grid ) {
var selection = grid.getSelectionModel().getSelection();
if( selection.length ) {
_self.store.remove( selection );
if( _self.useGridRowEditing ) {
_self.store.sync();
}
}
}
}
}
} );
}
Here is the remove function will remove the selected items, and I have store.add(item) to add records. But the problem is if I run the remove function, and then store.add to add any items, the store.add will fire create and destroy together. The second destroy will post exact data as the first time when I run the remove function.
I suppose that the store.add will only call the create api in the proxy, but why destroy has been called?
I see the remove throws an exception. If an exception has been thrown, is that mean the remove action is still pending? So it batch the add and remove together?
This is not caused by the ExtJS, it causes the server respond "[null]". The array of null is considered as an exception, I guess the exception causes the request becomes pending.
I hope someone can help me with this.
I have a Backbone based SPA for a responsive website with a .net WebAPI providing all of the data.
I've recently found a weird problem. I've added a search box, which searches one of the catalogues on the system. This works fine on desktop browsers and on Android. On iOS, executing a search seems to take you back to the sign in page.
You can execute a search in various ways, you can either hit enter or you can click the search icon. Both of these then trigger a method that navigates the router to the URL for the search result.
My first thought was that it was some button weirdness, but I don't think that's the problem as both methods of search execution are causing the same problem.
The search results are displayed in a view that is secured (It requires a username to be present - this is stored in a hidden field on the page). There are two search boxes on the site - one on the home page and one on the search results page itself (it shows a default set when you load it first time - which it does load first time fine). Both search boxes are exhibiting the same behaviour.
My site is set up in such a way that when Backbone pulls back a model, if it gets a 401 back from the API then it will send you back to the login page, so I can only think it's happening here.
Here's my view code...
function (SiansPlan, ErrorManager, RecipeSearchResult, Header, Components, TemplateSource) {
var recipeSearchView = SiansPlan.SecureView.extend({
name: 'Recipe Search',
sectionName: 'Recipes',
queryText: '',
template: Handlebars.compile(TemplateSource),
headerView: new Header({ text: 'Recipes', swatch: 'e' }),
searchBoxRegion: undefined,
$searchWrapper: undefined,
$queryHeaderMobile: undefined,
$queryHeaderDesktop: undefined,
$searchButton: undefined,
$searchInput: undefined,
$recipeSearch : undefined,
events: {
'click .link-container': 'showRecipe',
'click #searchWrapper': 'showSearch',
'click #searchButton': 'showOrPerformSearch',
'keydown #searchButton': 'performSearchOnEnter',
'keydown #recipeSearch': 'performSearchOnEnter'
},
initialize: function (options) {
this.options = options || {};
SiansPlan.SecureView.prototype.initialize.call(this, options);
this.queryText = Object.exists(this.options.query) ? this.options.query : '';
},
bindData: function () {
this.$el.html(this.template({ results: this.collection.toJSON() }));
},
render: function () {
var that = this;
if (this.isSecured()) {
this.trigger('rendering');
var params = {
success: function () {
that.bindData();
that.trigger('rendered');
},
error: function (model, xhr) {
if (Object.exists(xhr) && xhr.status == 401) {
that.applyTimedOutSecureLoginPrompt();
} else {
that.$el.html('Unable to fetch search results');
ErrorManager.handleXhr('Search failed', xhr);
}
that.trigger('rendered');
}
};
if (!Object.exists(this.collection)) {
this.collection = new RecipeSearchResult.Collection({ username: SiansPlanApp.session.username(), query: this.queryText });
}
this.collection.fetch(params);
} else {
this.applySecureLoginPrompt();
}
return this;
},
postRender: function () {
var that = this;
var queryHeader = "All recipes";
if (Object.hasValue(this.queryText)) {
queryHeader = this.collection.length + " results for '" + this.queryText + "'";
}
this.$searchWrapper = $('#searchWrapper');
this.$queryHeaderMobile = $('#queryHeaderMobile');
this.$queryHeaderDesktop = $('#queryHeaderDesktop');
this.$searchButton = $('#searchWrapper');
this.$searchInput = $('#searchInput');
this.$recipeSearch = $('#recipeSearch');
this.$queryHeaderMobile.html(queryHeader);
this.$queryHeaderDesktop.html(queryHeader);
this.$recipeSearch.val(this.queryText);
SiansPlanApp.session.waitForLoad(30, function () {
that.searchBoxRegion = new SiansPlan.Region({ el: '.recipe-search-box-container' });
that.searchBoxRegion.renderView(new Components.RecipeSearchBox({ username: SiansPlanApp.session.username(), query: that.queryText, title: 'Search' }));
});
},
performSearchOnEnter: function (e) {
if (e.keyCode == 13) {
this.showOrPerformSearch(e);
}
},
showOrPerformSearch: function (e) {
if (!this.$searchInput.is(':visible')) {
this.showSearch(e);
} else {
e.preventDefault();
var url = '/recipes/search/' + this.$recipeSearch.val();
window.SiansPlanApp.router.navigate(url, true);
}
return false;
},
showRecipe: function (e) {
e.preventDefault();
var url = $(e.target).find('a').first().attr('href');
window.SiansPlanApp.router.navigate(url, true);
},
showSearch: function (e) {
e.preventDefault();
if (!this.$searchInput.is(':visible')) {
this.$queryHeaderMobile.hide();
this.$searchInput.show();
this.$recipeSearch.focus();
this.$recipeSearch.select();
}
return false;
}
});
return recipeSearchView;
});
UPDATES
I've set up some alerts as follows in the script to see what's going on and I've discovered the following...
render: function () {
var that = this;
if (this.isSecured()) {
this.trigger('rendering');
var params = {
success: function () {
alert('Bind has succeeded!');
that.bindData();
that.trigger('rendered');
},
error: function (model, xhr) {
alert('Bind has failed!');
if (Object.exists(xhr) && xhr.status == 401) {
that.applyTimedOutSecureLoginPrompt();
} else {
that.$el.html('Unable to fetch search results');
ErrorManager.handleXhr('Search failed', xhr);
}
that.trigger('rendered');
alert(xhr.status + ' ' + xhr.responseText);
}
};
if (!Object.exists(this.collection)) {
alert('Binding new collection: ' + SiansPlanApp.session.username() + ' - ' + this.queryText);
this.collection = new RecipeSearchResult.Collection({ username: SiansPlanApp.session.username(), query: this.queryText });
}
alert('About to fetch using ' + this.collection.url());
this.collection.fetch(params);
} else {
alert('I dont appear to be secured??');
this.applySecureLoginPrompt();
}
return this;
},
When I first load the page (to show all the results) it loads fine and 'Bind Succeeded!' appears. The API call made is /api/recipes/search/{username}/
When I submit search criteria it fails ('Bind failed!') with the API call of /api/recipes/search/{username}/{query} and returns a 401.
This has me even more befuddled than before as this now looks like an API issue, but other devices are working fine and if I submit the same queries into Fiddler everything is, as expected, fine.
I've found the answer in the smallest place...
The issue was that the search criteria had an upper case letter. So, for example, when searching with 'Fish', The API generated a 301 which redirected to /api/recipes/search/{username}/fish. iOS didn't like that and reported it as a 401 (Which truly sucks!)