We are using ExtJS for a webapplication. In that application, we use the standard Ext.form.ComboBox control when a simple dropdown is required, and the Ext.us.Andrie.Select control when we need a dropdown where you can select multiple values and/or clear the value. Creating either of these always requires a bunch of boilerplate code for the config options, so I wanted a class that reduced boilerplate code and while I was at it, I wanted this class to be able to produce either the simple dropdown, or the more advanced one, depending on a config option (multi: true or clearable: true), but this turned out to be a lot harder than expected.
This is the closest I came to a working result:
MyComboBox = (function() {
var singleDefaults = {
typeAhead: false,
triggerAction: 'all',
selectOnFocus: false,
allowBlank: true,
editable: false,
delay: 700
};
var multiDefaults = {
typeAhead: false,
triggerAction: 'all',
selectOnFocus: false,
allowBlank: true,
editable: false,
delay: 700
};
var constructor = function(config) {
if (config.multi || config.clearable) {
config = Ext.apply(this, config, multiDefaults);
Ext.apply(this, Ext.ux.Andrie.Select.prototype);
Ext.apply(this, Ext.ux.Andrie.Select(config));
Ext.ux.Andrie.Select.prototype.constructor.call(this, config);
} else {
config = Ext.apply(this, config, singleDefaults);
Ext.apply(this, Ext.form.ComboBox.prototype);
Ext.apply(this, Ext.form.ComboBox(config));
Ext.form.ComboBox.prototype.constructor.call(this, config);
}
};
return function(config) {
this.constructor = constructor;
this.constructor(config);
};
})();
Well, it doesn't crash, but it doesn't really work either. When set to behave like Ext.ux.Andrie.Select, it wants to load the store even when it's loaded, doesn't expand the dropdown unless you start typing in the field.
Another approach that was tried was something like:
MyComboBox = Ext.extend(Ext.form.ComboBox, {
constructor: function(config){
if (config.multi || config.clearable) {
Ext.form.ComboBox.prototype.constructor.call(this, config);
} else {
Ext.ux.Andrie.Select.prototype.constructor.call(this, config);
}
}
});
That doesn't work because the Andrie dropdown doesn't define a constructor function of its own so it ends up calling the constructor function of Ext.form.ComboBox, which it inherits from, which results in a normal dropdown, not the multiselect dropdown.
I suppose this is ExtJS specific, but if you have a framework agnostic approach to doing this, I can probably translate it to ExtJS.
When building medium to large scale JavaScript applications, I find it convenient to move all control creation/instantiation into a single class were each method is a factory for that control (i.e.: combo box, text area, buttons, tool tips, check boxes, etc). Since only your factory class has the knowledge of how to create controls, this has the added benefit of decoupling your view logic from control instantiation themselves (that is, of course, as long as the control constructors and interfaces stay the same).
WidgetFactory = {
comboBox: function(config) {
return config.multi || config.clearable
? this.comboBoxMultiple(config)
: this.comboBoxSingle(config)
},
comboBoxSingle: function(config) {
// ... boiler plate goes here ...
},
comboBoxMultiple: function(config) {
// ... boiler plate goes here ...
},
textArea : function(config) {},
textBox : function(config) {},
checkbox : function(config) {},
radioButton : function(config) {},
button : function(config) {},
slider : function(config) {},
colorPicker : function(config) {}
// etc
};
Decoupling control creation from the view allows you quickly and easily swap out on implementation of a control for another without having to hunt through the whole of your application to find/replace all instances of that control's creation boilerplate.
Edit:
I didn't mean to imply that this would replace the use of objects for creating/instantiating controls, but rather, to be used in addition to. Using the WidgetFactory as a single gateway to all controls allows more flexibility than just being able to change super classes. You can swap out the entire class without altering the original class, allowing you to have multiple, different implementations if necessary.
For example, if your single combo box is defined by some class, namespace.ui.combobox.single, then you can do
comboBoxSingle: function(config) {
return new namespace.ui.combobox.single(config);
},
However, if you need to suddenly use a different class, for testing perhaps, you can change it once in the factory
comboBoxSingle: function(config) {
return new namespace.ui.combobox.single2(config);
},
and easily switch back and forth without having to alter the actual widget classes.
Almost forgot about this question. I solved this and if anyone is wondering, here's how to do it:
var constructor = function(config) {
if (config && (config.multi || config.clearable)) {
config = Ext.apply(this, config, multiDefaults);
Ext.applyIf(this, Ext.ux.Andrie.Select.prototype);
Ext.ux.Andrie.Select.createDelegate(this)(config);
} else {
config = Ext.apply(this, config, singleDefaults);
Ext.applyIf(this, Ext.form.ComboBox.prototype);
Ext.form.ComboBox.createDelegate(this)(config);
}
};
Related
Good day all.
I'm into a big project that uses EXTjs (i guess it's 4.0), the project is huge and have several years behind.
I'm not into Extjs so I'm trying to learn what to do and how to do it, and my new task is to create a persistent, global object, available into the whole application in which I need to store some information that are used in different parts of the project (let's say for example that the user can set a particular property of this object to "true" while doing some actions and this "true" it will be used into another viewcontroller to enable some functions, things like this).
so, I've created a new file called userJsonMainModel.js :
Ext.define('Tac3.userJsonMainModel', {
extend: 'Ext.data.Model',
constructor: function() {
var userJsonMainModel = this;
userJsonMainModel.callParent(arguments);
userJsonMainModel.data.tmp = {};
},
testProperty:{foo:"bar"},
testMethod: function (){
console.log("testFunction called");
}
});
and in Application.js :
requires: [
...
'Tac.userJsonMainModel'
],
stores: ['Countries', 'Kpis', 'Dimensions'],
autoCreateViewport: false,
init: function() {
var controller = this
Ext.tip.QuickTipManager.init();
Ext.setGlyphFontFamily('FontAwesome');
var userJsonMainModel = controller.createUserJsonMainModel();
console.log("into init: ", this.userJsonMainModel.testProperty);
...
createUserJsonMainModel: function() {
var controller = this;
controller.userJsonMainModel = Ext.create('Tac3.userJsonMainModel', {
controller: controller
});
console.log("check if the jsonmainmodel exist ",controller.userJsonMainModel.testProperty);
},
this is actually working, now the second step is to access the same object from another view (or its viewcontroller), this is what I've done into a a viewController:
Ext.define('Tac3.view.udesign.UdesignController', {
extend: 'Ext.app.ViewController',
alias: 'controller.udesign',
init: function(view) {
...
console.log("into init: ", this.userJsonMainModel.testProperty);
}
and this is actually throwing a:
Uncaught TypeError: Cannot read property 'testProperty' of undefined
I was pretty sure the objects defined into application.js would be globally accessible, but I guess I'm wrong, or doing something in a wrong way.
since I've found quite no examples on this topic (which is probably because it is not a standard way to do this), I'd like to ask what I'm doing wrong?
Just define a class and require it in your application:
Ext.define('MyApp.Globals', {
singleton: true,
foo: 100,
bar: 'baz'
});
I'm trying to get a custom extjs component to render either a green-check or red-x image, based on a true/false value being bound to it.
There's a couple of other controls that previous developers have written for rendering custom labels/custom buttons that I'm trying to base my control off but I'm not having much luck.
I'd like to be able to use it in a view as follows where "recordIsValid" is the name of the property in my model. (If I remove the xtype: it just renders as true/false)
{
"xtype": "booldisplayfield",
"name": "recordIsValid"
}
Here's what I have so far, but ExtJS is pretty foreign to me.
Ext.define('MyApp.view.ux.form.BoolDisplayField', {
extend: 'Ext.Component',
alias : 'widget.booldisplayfield',
renderTpl : '<img src="{value}" />',
autoEl: 'img',
config: {
value: ''
},
initComponent: function () {
var me = this;
me.callParent(arguments);
this.renderData = {
value: this.getValue()
};
},
getValue: function () {
return this.value;
},
setValue: function (v) {
if(v){
this.value = "/Images/booltrue.png";
}else{
this.value = "/Images/boolfalse.png";
}
return this;
}
});
I'd taken most of the above from a previous custom linkbutton implementation. I was assuming that setValue would be called when the model-value for recordIsValid is bound to the control. Then based on whether that was true or false, it would override setting the value property of the control with the correct image.
And then in the initComponent, it would set the renderData value by calling getValue and that this would be injected into the renderTpl string.
Any help would be greatly appreciated.
You should use the tpl option instead of the renderTpl one. The later is intended for rendering the component structure, rather that its content. This way, you'll be able to use the update method to update the component.
You also need to call initConfig in your component's constructor for the initial state to be applied.
Finally, I advice to use applyValue instead of setValue for semantical reasons, and to keep the boolean value for getValue/setValue.
Ext.define('MyApp.view.ux.form.BoolDisplayField', {
extend: 'Ext.Component',
alias : 'widget.booldisplayfield',
tpl: '<img src="{src}" />',
config: {
// I think you should keep the true value in there
// (in order for setValue/getValue to yield the expected
// result)
value: false
},
constructor: function(config) {
// will trigger applyValue
this.initConfig(config);
this.callParent(arguments);
},
// You can do this in setValue, but since you're using
// a config option (for value), it is semantically more
// appropriate to use applyValue. setValue & getValue
// will be generated anyway.
applyValue: function(v) {
if (v) {
this.update({
src: "/Images/booltrue.png"
});
}else{
this.update({
src: "/Images/boolfalse.png"
});
}
return v;
}
});
With that, you can set your value either at creation time, or later, using setValue.
// Initial value
var c = Ext.create('MyApp.view.ux.form.BoolDisplayField', {
renderTo: Ext.getBody()
,value: false
});
// ... that you can change later
c.setValue(true);
However, you won't be able to drop this component as it is in an Ext form and have it acting as a full fledged field. That is, its value won't be set, retrieved, etc. For that, you'll have to use the Ext.form.field.Field mixin. See this other question for an extended discussion on the subject.
The documentation for creating custom Kendo widgets is good enough, and leads to something like:
declare var kendo: kendo;
// To be able to get types, we can express the widget as this interface
interface ICustomDatePicker {
options: () => kendo.ui.DatePickerOptions;
}
; (function ($:JQueryStatic, window:any, document:Document, undefined?) {
var CustomDatePicker: ICustomDatePicker = (<any>kendo.ui.DatePicker).extend({
init: function (element, options:kendo.ui.DatePickerOptions) {
var self = this;
// base call to initialize widget
(<any>kendo.ui.DatePicker).fn.init.call(self, element, options);
},
options: {
// the name is what it will appear as off the kendo namespace (i.e. kendo.ui.CustomDatePicker).
// The jQuery plugin would be jQuery.fn.kendoCustomDatePicker.
name: "CustomDatePicker"
}
});
// This makes it work as a jQuery plugin
(<any>kendo.ui).plugin(CustomDatePicker);
})(jQuery, window, document);
A typescript file with that above, let's me do things like: $("#datePicker").kendoCustomDatePicker({}); and it all works beautifully.
My question is, is there a better way to write this in class form? My original thought is this:
module Foo {
class CustomDatePicker extends kendo.ui.DatePicker {
constructor(element, options) {
super(element, options);
}
}
(<any>kendo.ui).plugin(CustomDatePicker);
}
But that doesn't work (when calling the same $("#datePicker").kendoCustomDatePicker({});. This Gist gets closer, but I think the syntax is a bit funky - that the class doesn't extend the control directly. Any ideas?
Update 1
Looking at this answer, I'm trying to find a way to clean up setting the options by having it IN the class. I've gotten the following to semi-work, though it breaks down for some options and not others:
constructor(element: Element, options?: kendo.ui.TimePickerOptions) {
super(element, options);
$.extend(this.options, <kendo.ui.TimePickerOptions>{
format: "hh:mm tt",
parseFormats: ["HH:mm", "hh:mm tt"]
});
}
In this case, it respects that format and you can see it working. If you try and do the following:
$.extend(this.options, <kendo.ui.TimePickerOptions>{
format: "hh:mm tt",
parseFormats: ["HH:mm", "hh:mm tt"]
});
.. it doesn't work, doesn't parse the input at all.
There is a way but I am not 100% sure it qualifies as "nice". Here is some code which I wrote today and works:
class MyTreeView extends kendo.ui.TreeView
{
constructor(element: Element, options?: kendo.ui.TreeViewOptions) {
super(element, options);
}
static fn;
}
// kendo.ui.plugin relies on the fn.options.name to get the name of the widget
MyTreeView.fn = MyTreeView.prototype;
MyTreeView.fn.options.name = "MyTreeView";
// kendo.ui.plugin which comes with the Kendo TypeScript definitions doesn't include this overload
module kendo.ui {
export declare function plugin(widget: any, register?: Object, prefix?: String);
}
// register the plugin
kendo.ui.plugin(MyTreeView);
// extend the jQuery interface with the plugin method (needed to be used later)
interface JQuery {
kendoMyTreeView(options?: kendo.ui.TreeViewOptions): JQuery;
}
// use the plugin
$(function () {
$("#content").kendoMyTreeView({
dataSource: [
{
text: "Root",
items: [
{ text: "Child" }
]
}
]
});
});
Say, we have the following in a store:
{
"document": {
"success": "true",
"totalAllocation": "40000000.00",
"fundAllocation": [
{
"fundName": "Zais Opportunity Ltd Class B",
"allocation": "10000000.00"
},
{
"fundName": "Metacapital Mortgage Opportunities Ltd",
"allocation": "10000000.00"
},
...
]
}
}
And what I'd like to do is something like this:
itemTpl: Ext.create('Ext.XTemplate',
'<div>',
'<span>{fundName}</span>',
'<span>{[this.getPercentage(values.allocation, parent.totalAllocation)]}%</span>',
'</div>',
{
getPercentage: function (allocation, totalAllocation) {
return Ext.Number.toFixed(allocation / totalAllocation, 2);
}
}
)
But, of course, this doesn't work since 'parent' in this scope is empty.
Any idea how to fetch the value of the totalAllocation field inside XTemplate's fundtion to display the percentage allocated to the current fund in a list item?
Workarounds are welcomed as well.
From your data code it looks like document is the store root because there is a success property underneath it. Assuming that is the case, you can use the store reader's rawData property to get a reference to the value before you create the template. Then you can simply use the referenced value in the getPercentage function.
Your code does not show where you are creating this itemTpl in your class so I am assuming that you are creating this itemTpl inside the initComponent of the view you are instantiating.
I have no idea what type of component you are trying to create here other than the fact that it has an itemTpl config property which could be any subclass of Ext.view.AbstractView.
So I will assume that you are trying to use this in a gridpanel's view because that is the most common subclass of Ext.view.AbstractView.
Here's some code examples.
Example 1:
Ext.define('YourApp.view.TemplateGrid', {
extend: 'Ext.grid.Panel',
// column configs etc...
initComponent: function() {
var me = this,
templateStore = Ext.getStore('TemplateStoreId'),
totalAllocation = templateStore.proxy.reader.rawData.totalAllocation;
me.viewConfig.itemTpl = Ext.create('Ext.XTemplate',
'<div>',
'<span>{fundName}</span>',
'<span>{[this.getPercentage(values.allocation)]}%</span>',
'</div>',
{
getPercentage: function (allocation) {
return Ext.Number.toFixed(allocation / totalAllocation, 2);
}
}
)
}
});
Example 1 wouldn't work if you want to be able to load the store again (after initialization), it also assumes that your view store is already loaded. Here's another example showing the component set-up to handle multiple loads of the store without being recreated, it also assumes that the store is not loaded at the time when you create the view:
Example 2
Ext.define('YourApp.view.TemplateGrid', { // or whatever you are calling it
extend: 'Ext.grid.Panel',
// column configs etc...
totalAllocation = 0, // add this as a view property
initComponent: function() {
var me = this,
templateStore = Ext.create('YourApp.store.TemplateStore');
templateStore.on('load', function() {
me.totalAllocation = templateStore.proxy.reader.rawData.totalAllocation;
}
me.viewConfig.itemTpl = Ext.create('Ext.XTemplate',
'<div>',
'<span>{fundName}</span>',
'<span>{[this.getPercentage(values.allocation)]}%</span>',
'</div>',
{
getPercentage: function (allocation) {
return Ext.Number.toFixed(allocation / me.totalAllocation, 2);
}
}
)
templateStore.load();
me.callParent(arguments);
}
});
I have a simple TreePanel. I would like to select a particular node upon loading it. The nodes are from a remote file (json).
The tree is loading as expected. However, the node is not being selected. Firebug shows node as undefined. This perhaps because of the async property. But, I an unable to configure this other wise, or specify the node be selected.
Any suggestions welcomed, and thank you.
LeftMenuTree = new Ext.tree.TreePanel({
renderTo: 'TreeMenu',
collapsible: false,
height: 450,
border: false,
userArrows: true,
animate: true,
autoScroll: true,
id: 'testtest',
dataUrl: fileName,
root: {
nodeType: 'async',
iconCls:'home-icon',
expanded:true,
text: rootText
},
listeners: {
"click": {
fn: onPoseClick,
scope: this
}
},
"afterrender": {
fn: setNode,
scope: this
}
});
function setNode(){
alert (SelectedNode);
if (SelectedNode == "Orders"){
var treepanel = Ext.getCmp('testtest');
var node = treepanel.getNodeById("PendingItems");
node.select();
}
}
I use this code in the TreeGrid to select a node
_I.treeGrid.getSelectionModel().select(_I.treeGrid.getRootNode());
I haven't tried this in a TreePanel but since the TreeGrid is based on it I'll just assume this works. I used the load event of the loader to plugin similar code after the XHR request was done, so try to write your setNode function like this:
var loader = LeftMenuTree.getLoader();
loader.on("load", setNode);
function setNode(){
alert (SelectedNode);
if (SelectedNode == "Orders"){
var treepanel = Ext.getCmp('testtest');
treepanel.getSelectionModel().select(treepanel.getNodeById("PendingItems"));
}
}
this work for me...
var loader = Ext.getCmp('testtest').getLoader();
loader.on("load", function(a,b,c){
b.findChild("id",1, true).select(); // can find by any parameter in the json
});
I have documented a way to accomplish something very similar here:
http://www.sourcepole.ch/2010/9/28/understanding-what-s-going-on-in-extjs
what you'll need to make sure is that the node that you are selecting is visible. You can accomplish that by traversing the tree and node.expand()ing all the nodes parents (from the root down).
This is because the node isn't really selectable until the tree has been rendered. Try adding your node selection to an event listener listening for the render event.
If you're using a recent enough version of ExtJS then I find using ViewModels and the Selection config far easier for this kind of thing.
Something like:
LeftMenuTree = new Ext.tree.TreePanel({
renderTo: 'TreeMenu',
collapsible: false,
height: 450,
border: false,
userArrows: true,
animate: true,
autoScroll: true,
id: 'testtest',
dataUrl: fileName,
bind: {
Selection: '{SelectedNode}'
},
root: {
nodeType: 'async',
iconCls:'home-icon',
expanded:true,
text: rootText
},
listeners: {
"click": {
fn: onPoseClick,
scope: this
}
"afterrender": {
fn: setNode,
scope: this
}
});
(You'll need to either have a ViewModel set up in the TreePanel or the owning view)
Then assuming you're using a ViewController and setNode is a member:
setNode: function(){
var nodeToSelect = // code to find the node object here
this.getViewModel().set('Selection', nodeToSelect);
}
The nice thing about the ViewModel approach is that it seems to just handle all of the rendering / data loading issues automatically.