I want to create custom ExtJS form field components using other ExtJS components in it (e.g. TreePanel). How can I do it most easily?
I've read docs of Ext.form.field.Base but I don't want to define field body by fieldSubTpl. I just want to write code which creates ExtJS components and maybe some other code which gets and sets values.
Update: Summarized purposes are the followings:
This new component should fit in the
form GUI as a field. It should have
label and the same alignment (label,
anchor) of other fields without need
of further hacking.
Possibly, I have
to write some getValue, setValue
logic. I'd rather embed it into this component than making separated code which copies things into further hidden form fields that I also have to manage.
To extend #RobAgar 's answer, following a really simple Date Time field that I wrote for ExtJS 3 and it's quickport that I made for ExtJS 4. The important thing is the use of the Ext.form.field.Field mixin. This mixin provides a common interface for the logical behavior and state of form fields, including:
Getter and setter methods for field values
Events and methods for tracking value and validity changes
Methods for triggering validation
This can be used for combining multiple fields and let act them as one. For a total custom fieldtype I recommend to extend Ext.form.field.Base
Here is the example that I mentioned above. It should shoe how easy this can be done even for something like a date object where we need to format the data within the getter and setter.
Ext.define('QWA.form.field.DateTime', {
extend: 'Ext.form.FieldContainer',
mixins: {
field: 'Ext.form.field.Field'
},
alias: 'widget.datetimefield',
layout: 'hbox',
width: 200,
height: 22,
combineErrors: true,
msgTarget: 'side',
submitFormat: 'c',
dateCfg: null,
timeCfg: null,
initComponent: function () {
var me = this;
if (!me.dateCfg) me.dateCfg = {};
if (!me.timeCfg) me.timeCfg = {};
me.buildField();
me.callParent();
me.dateField = me.down('datefield')
me.timeField = me.down('timefield')
me.initField();
},
//#private
buildField: function () {
var me = this;
me.items = [
Ext.apply({
xtype: 'datefield',
submitValue: false,
format: 'd.m.Y',
width: 100,
flex: 2
}, me.dateCfg),
Ext.apply({
xtype: 'timefield',
submitValue: false,
format: 'H:i',
width: 80,
flex: 1
}, me.timeCfg)]
},
getValue: function () {
var me = this,
value,
date = me.dateField.getSubmitValue(),
dateFormat = me.dateField.format,
time = me.timeField.getSubmitValue(),
timeFormat = me.timeField.format;
if (date) {
if (time) {
value = Ext.Date.parse(date + ' ' + time, me.getFormat());
} else {
value = me.dateField.getValue();
}
}
return value;
},
setValue: function (value) {
var me = this;
me.dateField.setValue(value);
me.timeField.setValue(value);
},
getSubmitData: function () {
var me = this,
data = null;
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
data = {},
value = me.getValue(),
data[me.getName()] = '' + value ? Ext.Date.format(value, me.submitFormat) : null;
}
return data;
},
getFormat: function () {
var me = this;
return (me.dateField.submitFormat || me.dateField.format) + " " + (me.timeField.submitFormat || me.timeField.format)
}
});
Now that's cool. The other day, I created a fiddle to answer another question before realizing I was off-topic. And here your are, finally bringing to my attention the question to my answer. Thanks!
So, here are the steps required in implementing a custom field from another component:
Creating the child component
Render the child component
Ensuring the child component is sized and resized correctly
Getting and setting value
Relaying events
Creating the child component
The first part, creating the component, is easy. There's nothing particular compared to creating a component for any other usage.
However, you must create the child in the parent field's initComponent method (and not at rendering time). This is because external code can legitimately expect that all dependent objects of a component are instantiated after initComponent (e.g. to add listeners to them).
Furthermore, you can be kind to yourself and create the child before calling the super method. If you create the child after the super method, you may get a call to your field's setValue method (see bellow) at a time when the child is not yet instantiated.
initComponent: function() {
this.childComponent = Ext.create(...);
this.callParent(arguments);
}
As you see, I am creating a single component, which is what you'll want in most case. But you can also want to go fancy and compose multiple child components. In this case, I think it would be clever to back to well known territories as quickly as possible: that is, create one container as the child component, and compose in it.
Rendering
Then comes the question of rendering. At first I considered using fieldSubTpl to render a container div, and have the child component render itself in it. However, we don't need the template features in that case, so we can as well bypass it completely using the getSubTplMarkup method.
I explored other components in Ext to see how they manage the rendering of child components. I found a good example in BoundList and its paging toolbar (see the code). So, in order to obtain the child component's markup, we can use Ext.DomHelper.generateMarkup in combination with the child's getRenderTree method.
So, here's the implementation of getSubTplMarkup for our field:
getSubTplMarkup: function() {
// generateMarkup will append to the passed empty array and return it
var buffer = Ext.DomHelper.generateMarkup(this.childComponent.getRenderTree(), []);
// but we want to return a single string
return buffer.join('');
}
Now, that's not enough. The code of BoundList learns us that there's another important part in component rendering: calling the finishRender() method of the child component. Fortunately, our custom field will have its own finishRenderChildren method called just when that needs to be done.
finishRenderChildren: function() {
this.callParent(arguments);
this.childComponent.finishRender();
}
Resizing
Now our child will be rendered in the right place, but it will not respect its parent field size. That is especially annoying in the case of a form field, because that means it won't honor the anchor layout.
That's very straightforward to fix, we just need to resize the child when the parent field is resized. From my experience, this is something that was greatly improved since Ext3. Here, we just need to not forget the extra space for the label:
onResize: function(w, h) {
this.callParent(arguments);
this.childComponent.setSize(w - this.getLabelWidth(), h);
}
Handling value
This part will, of course, depend on your child component(s), and the field you're creating. Moreover, from now on, it's just a matter of using your child components in a regular way, so I won't detail this part too much.
A minima, you also need to implement the getValue and setValue methods of your field. That will make the getFieldValues method of the form work, and that will be enough to load/update records from the form.
To handle validation, you must implement getErrors. To polish this aspect, you may want to add a handful of CSS rules to visually represent the invalid state of your field.
Then, if you want your field to be usable in a form that will be submitted as an actual form (as opposed to with an AJAX request), you'll need getSubmitValue to return a value that can be casted to a string without damage.
Apart from that, as far as I know, you don't have to worry about the concept or raw value introduced by Ext.form.field.Base since that's only used to handle the representation of the value in an actual input element. With our Ext component as input, we're way off that road!
Events
Your last job will be to implement the events for your fields. You will probably want to fire the three events of Ext.form.field.Field, that is change, dirtychange and validitychange.
Again, the implementation will be very specific to the child component you use and, to be honest, I haven't explored this aspect too much. So I'll let you wire this for yourself.
My preliminary conclusion though, is that Ext.form.field.Field offers to do all the heavy lifting for you, provided that (1) you call checkChange when needed, and (2) isEqual implementation is working with your field's value format.
Example: TODO list field
Finally, here's a complete code example, using a grid to represent a TODO list field.
You can see it live on jsFiddle, where I tries to show that the field behaves in an orderly manner.
Ext.define('My.form.field.TodoList', {
// Extend from Ext.form.field.Base for all the label related business
extend: 'Ext.form.field.Base'
,alias: 'widget.todolist'
// --- Child component creation ---
,initComponent: function() {
// Create the component
// This is better to do it here in initComponent, because it is a legitimate
// expectationfor external code that all dependant objects are created after
// initComponent (to add listeners, etc.)
// I will use this.grid for semantical access (value), and this.childComponent
// for generic issues (rendering)
this.grid = this.childComponent = Ext.create('Ext.grid.Panel', {
hideHeaders: true
,columns: [{dataIndex: 'value', flex: 1}]
,store: {
fields: ['value']
,data: []
}
,height: this.height || 150
,width: this.width || 150
,tbar: [{
text: 'Add'
,scope: this
,handler: function() {
var value = prompt("Value?");
if (value !== null) {
this.grid.getStore().add({value: value});
}
}
},{
text: "Remove"
,itemId: 'removeButton'
,disabled: true // initial state
,scope: this
,handler: function() {
var grid = this.grid,
selModel = grid.getSelectionModel(),
store = grid.getStore();
store.remove(selModel.getSelection());
}
}]
,listeners: {
scope: this
,selectionchange: function(selModel, selection) {
var removeButton = this.grid.down('#removeButton');
removeButton.setDisabled(Ext.isEmpty(selection));
}
}
});
// field events
this.grid.store.on({
scope: this
,datachanged: this.checkChange
});
this.callParent(arguments);
}
// --- Rendering ---
// Generates the child component markup and let Ext.form.field.Base handle the rest
,getSubTplMarkup: function() {
// generateMarkup will append to the passed empty array and return it
var buffer = Ext.DomHelper.generateMarkup(this.childComponent.getRenderTree(), []);
// but we want to return a single string
return buffer.join('');
}
// Regular containers implements this method to call finishRender for each of their
// child, and we need to do the same for the component to display smoothly
,finishRenderChildren: function() {
this.callParent(arguments);
this.childComponent.finishRender();
}
// --- Resizing ---
// This is important for layout notably
,onResize: function(w, h) {
this.callParent(arguments);
this.childComponent.setSize(w - this.getLabelWidth(), h);
}
// --- Value handling ---
// This part will be specific to your component of course
,setValue: function(values) {
var data = [];
if (values) {
Ext.each(values, function(value) {
data.push({value: value});
});
}
this.grid.getStore().loadData(data);
}
,getValue: function() {
var data = [];
this.grid.getStore().each(function(record) {
data.push(record.get('value'));
});
return data;
}
,getSubmitValue: function() {
return this.getValue().join(',');
}
});
Heh. After posting the bounty I found out that Ext.form.FieldContainer isn't just a field container, but a fully fledged component container, so there is a simple solution.
All you need to do is extend FieldContainer, overriding initComponent to add the child components, and implement setValue, getValue and the validation methods as appropriate for your value data type.
Here's an example with a grid whose value is a list of name/value pair objects:
Ext.define('MyApp.widget.MyGridField', {
extend: 'Ext.form.FieldContainer',
alias: 'widget.mygridfield',
layout: 'fit',
initComponent: function()
{
this.callParent(arguments);
this.valueGrid = Ext.widget({
xtype: 'grid',
store: Ext.create('Ext.data.JsonStore', {
fields: ['name', 'value'],
data: this.value
}),
columns: [
{
text: 'Name',
dataIndex: 'name',
flex: 3
},
{
text: 'Value',
dataIndex: 'value',
flex: 1
}
]
});
this.add(this.valueGrid);
},
setValue: function(value)
{
this.valueGrid.getStore().loadData(value);
},
getValue: function()
{
// left as an exercise for the reader :P
}
});
I've done this a few times. Here is the general process/pseudo-code I use:
Create an extension of field that provides the most useful re-use (typically Ext.form.TextField if you just want to get/set a string value)
In the afterrender of the field, hide the textfield, and create a wrapping element around this.el with this.wrap = this.resizeEl = this.positionEl = this.el.wrap()
Render any components to this.wrap (e.g. using renderTo: this.wrap in the config)
Override getValue and setValue to talk to the component(s) you rendered manually
You may need to do some manually sizing in a resize listener if your form's layout changes
Don't forget to cleanup any components you create in the beforeDestroy method!
I can't wait to switch our codebase to ExtJS 4, where these kinds of things are easy.
Good luck!
Since the question was asked rather vague - I only can provide the basic pattern for ExtJS v4.
Even if it's not too specific, it has the advance that it's rather universal like this:
Ext.define('app.view.form.field.CustomField', {
extend: 'Ext.form.field.Base',
requires: [
/* require further components */
],
/* custom configs & callbacks */
getValue: function(v){
/* override function getValue() */
},
setValue: function(v){
/* override function setValue() */
},
getSubTplData: [
/* most likely needs to be overridden */
],
initComponent: function(){
/* further code on event initComponent */
this.callParent(arguments);
}
});
The file /ext/src/form/field/Base.js provides the names of all configs and functions that can be overridden.
Following the documentation at http://docs.sencha.com/ext-js/4-0/#/api/Ext.form.field.Base
This code will create a reusable TypeAhead/Autocomplete style field for selecting a language.
var langs = Ext.create( 'Ext.data.store', {
fields: [ 'label', 'code' ],
data: [
{ code: 'eng', label: 'English' },
{ code: 'ger', label: 'German' },
{ code: 'chi', label: 'Chinese' },
{ code: 'ukr', label: 'Ukranian' },
{ code: 'rus', label: 'Russian' }
]
} );
Ext.define( 'Ext.form.LangSelector', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.LangSelector',
allowBlank: false,
hideTrigger: true,
width: 225,
displayField: 'label',
valueField: 'code',
forceSelection: true,
minChars: 1,
store: langs
} );
You can use the field in a form simply by setting the xtype to the widget name:
{
xtype: 'LangSelector'
fieldLabel: 'Language',
name: 'lang'
}
Many of the answers either use the Mixin Ext.form.field.Field or just extends on some already made class that suits their needs - which is fine.
But I do not recommend fully overwriting the setValue method, that is IMO really bad form!
A lot more happens than just setting and getting the value, and if you fully overwrite it - well you will for instance mess up the dirty state, processing of rawValue etc..
Two options here I guess, one is to callParent(arguments) inside the method you declare to keep things streamlined, or to at the end when you are done apply the inherited method from where ever you got it (mixin or extend).
But do not just overwrite it with no regards for what that already made method does behind the scenes.
Also remember that if you use other field types in your new class - then do set the isFormField property to false - otherwise your getValues method on the form will take those values and run with em!
Another solution could be this tree-field implementation.
It behaves just like a normal form field:
https://github.com/wencywww/Ext.ux.form.field.Tree
Here is an example of a custom panel that extends an Ext Panel. You can extend any component, check the docs for the fields, methods and events you can play with.
Ext.ns('yournamespace');
yournamespace.MyPanel = function(config) {
yournamespace.MyPanel.superclass.constructor.call(this, config);
}
Ext.extend(yournamespace.MyPanel, Ext.Panel, {
myGlobalVariable : undefined,
constructor : function(config) {
yournamespace.MyPanel.superclass.constructor.apply(this, config);
},
initComponent : function() {
this.comboBox = new Ext.form.ComboBox({
fieldLabel: "MyCombo",
store: someStore,
displayField:'My Label',
typeAhead: true,
mode: 'local',
forceSelection: true,
triggerAction: 'all',
emptyText:'',
selectOnFocus:true,
tabIndex: 1,
width: 200
});
// configure the grid
Ext.apply(this, {
listeners: {
'activate': function(p) {
p.doLayout();
},
single:true
},
xtype:"form",
border: false,
layout:"absolute",
labelAlign:"top",
bodyStyle:"padding: 15px",
width: 350,
height: 75,
items:[{
xtype:"panel",
layout:"form",
x:"10",
y:"10",
labelAlign:"top",
border:false,
items:[this.comboBox]
},
{
xtype:"panel",
layout:"form",
x:"230",
y:"26",
labelAlign:"top",
border:false,
items:[{
xtype:'button',
handler: this.someAction.createDelegate(this),
text: 'Some Action'
}]
}]
}); // eo apply
yournamespace.MyPanel.superclass.initComponent.apply(this, arguments);
this.comboBox.on('select', function(combo, record, index) {
this.myGlobalVariable = record.get("something");
}, this);
}, // eo function initComponent
someAction : function() {
//do something
},
getMyGlobalVariable : function() {
return this.myGlobalVariable;
}
}); // eo extend
Ext.reg('mypanel', yournamespace.MyPanel);
Could you describe the UI requirements that you have a bit more? Are you sure that you even need to do build an entire field to support the TreePanel? Why not set the value of a hidden field (see the "hidden" xtype in the API) from a click handler on a normal tree panel?
To answer your question more fully, you can find many tutorials on how to extend ExtJS components. You do this by leveraging the Ext.override() or Ext.Extend() methods.
But my feeling is that you may be over-complicating your design. You can achieve what you need to do by setting a value to this hidden field. If you have complex data, you can set the value as some XML or JSON string.
EDIT Here's a few tutorials. I highly recommend going with the KISS rule when it comes to your UI design. Keep It Simple Stupid!
Extending components using panels
I am trying to implement a drag and drop senario from an extJs TreePanel into a div in the body of the page. I have been following an example by Saki here.
So far I have the below code:
var contentAreas = new Array();
var tree = new Ext.tree.TreePanel({
title : 'Widgets',
useArrows: true,
autoScroll: true,
animate: true,
enableDrag: true,
border: false,
layout:'fit',
ddGroup:'t2div',
loader:new Ext.tree.TreeLoader(),
root:new Ext.tree.AsyncTreeNode({
expanded:true,
leaf:false,
text:'Tree Root',
children:children
}),
listeners:{
startdrag:function() {
$('.content-area').css("outline", "5px solid #FFE767");
},
enddrag:function() {
$('.content-area').css("outline", "0");
}
}
});
var areaDivs = Ext.select('.content-area', true);
Ext.each(areaDivs, function(el) {
var dd = new Ext.dd.DropTarget(el, {
ddGroup:'t2div',
notifyDrop:function(ddt, e, node) {
alert('Drop');
return true;
}
});
contentAreas[contentAreas.length] = dd;
});
The drag begins and the div highlights but when I get over the div it does not show as a valid drop target and the drop fails.
This is my first foray into extJS. I'm JQuery through and through and I am struggling at the moment.
Any help would be appreciated.
Ian
Edit
Furthermore if I create a panel with a drop target in it, this works fine. What is the difference between creating an element and selecting an existing element from the dom. This is obviously where I am going wrong but I'm none the wiser. I have to be able to select existing dom elements and make them into drop targets so the code below is not an option.
Here is the drop target that works
var target = new Ext.Panel({
renderTo: document.body
,layout:'fit'
,id:'target'
,bodyStyle:'font-size:13px'
,title:'Drop Target'
,html:'<div class="drop-target" '
+'style="border:1px silver solid;margin:20px;padding:8px;height:140px">'
+'Drop a node here. I\'m the DropTarget.</div>'
// setup drop target after we're rendered
,afterRender:function() {
Ext.Panel.prototype.afterRender.apply(this, arguments);
this.dropTarget = this.body.child('div.drop-target');
var dd = new Ext.dd.DropTarget(this.dropTarget, {
// must be same as for tree
ddGroup:'t2div'
// what to do when user drops a node here
,notifyDrop:function(dd, e, node) {
alert('drop');
return true;
} // eo function notifyDrop
});
}
});
See if adding true as the second param here makes any difference:
var areaDivs = Ext.select('.content-area', true);
As a cosmetic note, the param name e conventionally indicates an event object (as in the second arg of notifyDrop). For an element, el is more typical. Doesn't matter functionally, but looks weird to someone used to Ext code to see e passed into the DropTarget constructor.
If you are having problem duplicating a working example such as that, copy the entire thing, then modify it to your needs line-by-line - you can't go wrong.
As i know you can't set DropZone to any Ext element, just to Ext component. So this might be you problem. Try to use DropTarget instead of DropZone.