PDA

View Full Version : A Field class which does not have to be rendered in a FormLayout



Animal
7 Aug 2009, 6:20 AM
This extension to Ext.form.Field wraps itself in all the correct DOM elements, and creates its label regardless of what layout it is in.

It hides the whole thing on hide, and clean up after itself on destroy.

So no need for extra form layout Containers just to get Fields renderning properly.



Ext.ux.SelfLabelingTextField = Ext.extend(Ext.form.TextField, {

margins: '0 0 5 0', // to keep things right when using Box layouts

actionMode: 'itemCt',

onRender: function() {
Ext.ux.SelfLabelingTextField.superclass.onRender.apply(this, arguments);
this.resizeEl = this.el.wrap({
cls: 'x-form-element'
});
this.positionEl = this.itemCt = this.resizeEl.wrap({
cls: 'x-form-item '
});

// If we are hiding labels, then we're done!
if (!Ext.isDefined(this.hideLabels)) {
this.hideLabels = this.getParentProperty("hideLabels");
}
if (this.hideLabels) {
this.resizeEl.setStyle('padding-left', '0px');
return;
}

// Collect info we need to render the label.
if (!Ext.isDefined(this.labelWidth)) {
this.labelWidth = this.getParentProperty("labelWidth") || 100;
}
if (!Ext.isDefined(this.labelSeparator)) {
this.labelSeparator = this.getParentProperty("labelSeparator");
}
if (!Ext.isDefined(this.labelPad)) {
this.labelPad = this.getParentProperty("labelPad");
}
this.resizeEl.setStyle('padding-left', (this.labelWidth + (this.labelPad || 5)) + 'px');
this.itemCt.insertFirst({
tag: 'label',
cls: 'x-form-item-label',
style: {
width: this.labelWidth + 'px'
},
html: this.fieldLabel + (this.labelSeparator || ':')
});
},

// private
// Pulls a named property down from the first ancestor Container it's found in
getParentProperty: function(propName) {
for (var p = this.ownerCt; p; p = p.ownerCt) {
if (p[propName]) {
return p[propName];
}
}
},

// private
// Ensure the input field is sized to fit in the content area of the resizeEl (to the right of its padding-left)
onResize: function() {
Ext.ux.SelfLabelingTextField.superclass.onResize.apply(this, arguments);
this.el.setWidth(this.resizeEl.getWidth(true));
},

// private
// Ensure that we clean up on destroy.
onDestroy: function() {
Ext.ux.SelfLabelingTextField.superclass.onDestroy.apply(this, arguments);
this.itemCt.remove();
}
});
Ext.reg('selfLabelingTextfield', Ext.ux.SelfLabelingTextField);

SamuraiJack1
9 Aug 2009, 11:02 PM
At last.. A long awaited extension.

Condor
9 Aug 2009, 11:11 PM
Could be useful, but you should rewrite this as a plugin so you don't have to create a descendant for every field class you want to use.

Animal
9 Aug 2009, 11:53 PM
Could be useful, but you should rewrite this as a plugin so you don't have to create a descendant for every field class you want to use.

Yes, I'll look into that. It leaves TriggerFields high and dry.

I was doing this at the same time as I was doing my self replicating Field.

For the same example in fact. I'm getting togeter a vbox example with a self replicating "To" field. The vbox layout automatically manages the height of the mail body regardless of how many input fields there are above it. The current anchoring.js demo uses a "magic number" to anchor the body in a fixed position.

Mailto fields are often TriggerFields, so this will be essential I think.

Animal
10 Aug 2009, 12:10 AM
Here:



Ext.ux.FieldLabeller = (function(){

// Pulls a named property down from the first ancestor Container it's found in
function getParentProperty(propName) {
for (var p = this.ownerCt; p; p = p.ownerCt) {
if (p[propName]) {
return p[propName];
}
}
}

return {

// Add behaviour at important points in the Field's lifecycle.
init: function(f) {
f.onRender = f.onRender.createSequence(this.onRender);
f.onResize = f.onResize.createSequence(this.onResize);
f.onDestroy = f.onDestroy.createSequence(this.onDestroy);
},

onRender: function() {
// Do nothing if being rendered by a form layout
if (this.ownerCt) {
if (this.ownerCt.layout instanceof Ext.layout.FormLayout) {
return;
}
if (this.nextSibling()) {
this.margins = '0 0 5 0';
}
}

this.resizeEl = this.el.wrap({
cls: 'x-form-element'
});
this.positionEl = this.itemCt = this.resizeEl.wrap({
cls: 'x-form-item '
});
this.actionMode = 'itemCt';

// If we are hiding labels, then we're done!
if (!Ext.isDefined(this.hideLabels)) {
this.hideLabels = getParentProperty.call(this, "hideLabels");
}
if (this.hideLabels) {
this.resizeEl.setStyle('padding-left', '0px');
return;
}

// Collect info we need to render the label.
if (!Ext.isDefined(this.labelWidth)) {
this.labelWidth = getParentProperty.call(this, "labelWidth") || 100;
}
if (!Ext.isDefined(this.labelSeparator)) {
this.labelSeparator = getParentProperty.call(this, "labelSeparator");
}
if (!Ext.isDefined(this.labelPad)) {
this.labelPad = getParentProperty.call(this, "labelPad");
}
if (!Ext.isDefined(this.labelAlign)) {
this.labelAlign = getParentProperty.call(this, "labelAlign") || 'left';
}
this.itemCt.addClass('x-form-label-' + this.labelAlign);

if(this.labelAlign == 'top'){
this.labelWidth = 'auto';
this.resizeEl.setStyle('padding-left', '0px');
} else {
this.labelWidth += 'px';
this.resizeEl.setStyle('padding-left', (this.labelWidth + (this.labelPad || 5)) + 'px');
}

this.label = this.itemCt.insertFirst({
tag: 'label',
cls: 'x-form-item-label',
style: {
width: this.labelWidth
},
html: this.fieldLabel + (this.labelSeparator || ':')
});
},

// private
// Ensure the input field is sized to fit in the content area of the resizeEl (to the right of its padding-left)
onResize: function() {
this.el.setWidth(this.resizeEl.getWidth(true));
if (this.el.dom.tagName.toLowerCase() == 'textarea') {
var h = this.resizeEl.getHeight(true);
if (this.labelAlign == 'top') {
h -= this.label.getHeight();
}
this.el.setHeight(h);
}
},

// private
// Ensure that we clean up on destroy.
onDestroy: function() {
this.itemCt.remove();
}
};
})();

Animal
10 Aug 2009, 12:16 AM
Drop the following file into examples/form as vbox-form.html

It's a nice demo of plugin code and vbox behaviour.

I'm going to suggest it be added to SVN as an example.



<html>
<head>
<title>Vbox</title>
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css"/>

<!-- GC -->
<!-- LIBS -->
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<!-- ENDLIBS -->

<script type="text/javascript" src="../../ext-all-debug.js"></script>

<script type="text/javascript" src="vbox-form.js"></script>
<link rel="stylesheet" type="text/css" href="forms.css"/>

<!-- Common Styles for the examples -->
<link rel="stylesheet" type="text/css" href="../shared/examples.css"/>
</head>
<body>
<script type="text/javascript" src="../shared/examples.js"></script>
<!-- EXAMPLES -->
<h1>vbox Layout with Forms</h1>
<p>The <code><b>align:'stretch'</b></code> config of the vbox layout manager ensures
all child items are 100% of the container width.</p>
<p>The <code><b>flex</b></code> config of child items of a vbox layout specifies what share of the vertical
space to allocate for that child item.</p>
<p>The js is not minified so it is readable. See <a href="vbox-form.js">vbox-form.js</a>.</p>
</body>
</html>


with this as vbox-form.js:



Ext.Container.prototype.bufferResize = false;

Ext.ux.FieldLabeller = (function(){

// Pulls a named property down from the first ancestor Container it's found in
function getParentProperty(propName) {
for (var p = this.ownerCt; p; p = p.ownerCt) {
if (p[propName]) {
return p[propName];
}
}
}

return {

// Add behaviour at important points in the Field's lifecycle.
init: function(f) {
f.onRender = f.onRender.createSequence(this.onRender);
f.onResize = f.onResize.createSequence(this.onResize);
f.onDestroy = f.onDestroy.createSequence(this.onDestroy);
},

onRender: function() {
// Do nothing if being rendered by a form layout
if (this.ownerCt) {
if (this.ownerCt.layout instanceof Ext.layout.FormLayout) {
return;
}
if (this.nextSibling()) {
this.margins = '0 0 5 0';
}
}

this.resizeEl = this.el.wrap({
cls: 'x-form-element'
});
this.positionEl = this.itemCt = this.resizeEl.wrap({
cls: 'x-form-item '
});
this.actionMode = 'itemCt';

// If we are hiding labels, then we're done!
if (!Ext.isDefined(this.hideLabels)) {
this.hideLabels = getParentProperty.call(this, "hideLabels");
}
if (this.hideLabels) {
this.resizeEl.setStyle('padding-left', '0px');
return;
}

// Collect info we need to render the label.
if (!Ext.isDefined(this.labelWidth)) {
this.labelWidth = getParentProperty.call(this, "labelWidth") || 100;
}
if (!Ext.isDefined(this.labelSeparator)) {
this.labelSeparator = getParentProperty.call(this, "labelSeparator");
}
if (!Ext.isDefined(this.labelPad)) {
this.labelPad = getParentProperty.call(this, "labelPad");
}
if (!Ext.isDefined(this.labelAlign)) {
this.labelAlign = getParentProperty.call(this, "labelAlign") || 'left';
}
this.itemCt.addClass('x-form-label-' + this.labelAlign);

if(this.labelAlign == 'top'){
this.labelWidth = 'auto';
this.resizeEl.setStyle('padding-left', '0px');
} else {
this.labelWidth += 'px';
this.resizeEl.setStyle('padding-left', (this.labelWidth + (this.labelPad || 5)) + 'px');
}

this.label = this.itemCt.insertFirst({
tag: 'label',
cls: 'x-form-item-label',
style: {
width: this.labelWidth
},
html: this.fieldLabel + (this.labelSeparator || ':')
});
},

// private
// Ensure the input field is sized to fit in the content area of the resizeEl (to the right of its padding-left)
onResize: function() {
this.el.setWidth(this.resizeEl.getWidth(true));
if (this.el.dom.tagName.toLowerCase() == 'textarea') {
var h = this.resizeEl.getHeight(true);
if (this.labelAlign == 'top') {
h -= this.label.getHeight();
}
this.el.setHeight(h);
}
},

// private
// Ensure that we clean up on destroy.
onDestroy: function() {
this.itemCt.remove();
}
};
})();

Ext.ux.FieldReplicator = {
init: function(f) {
f.replicator = this;
f.enableKeyEvents = true;
f.on('change', this.onChange, this);
f.onKeyDown = f.onKeyDown.createInterceptor(this.onKeyDown);
},

// If tabbing out and the change event will be fired, flag that
// the change handler must focus the correct sibling Field.
onKeyDown: function(e) {
if ((e.getKey() == Ext.EventObject.TAB) && (String(this.startValue) !== String(this.getValue()))) {
if (e.shiftKey) {
this.focusPrev = true;
} else {
this.focusNext = true;
}
}
},

// Handle the field either being changed to blank or from blank.
onChange: function(f, n, o) {
var c = f.ownerCt, l,
ps = f.previousSibling(),
ns = f.nextSibling();
if (Ext.isEmpty(n)) {
if (!Ext.isEmpty(o)) {
// The Field has been blanked, and it is not the only one left, remove it
if ((ps && (ps.replicator === this)) || (ns && (ns.replicator === this))) {
l = f.findParentBy(function(p) {
return !Ext.isDefined(p.ownerCt);
});
c.remove(f);
l.doLayout();
}
}
} else {
if (Ext.isEmpty(o)) {
// Field filled, insert a clone as the next sibling
ns = new f.constructor(f.cloneConfig());
c.insert(c.items.indexOf(f) + 1, ns);
c.doLayout();
l = f.findParentBy(function(p) {
return !Ext.isDefined(p.ownerCt);
});
l.doLayout();
}
}
if (f.focusPrev) {
delete f.focusPrev;
ps.focus(false, true);
} else if (f.focusNext) {
delete f.focusNext;
ns.focus(false, true);
}
}
};

Ext.onReady(function() {
var form = new Ext.form.FormPanel({
baseCls: 'x-plain',
labelWidth: 55,
url:'save-form.php',
layout: {
type: 'vbox',
align: 'stretch'
},
defaults: {
xtype: 'textfield',
anchor: '100%' // anchor width by percentage
},

items: [{
plugins: [ Ext.ux.FieldReplicator, Ext.ux.FieldLabeller ],
fieldLabel: 'Send To',
name: 'to'
},{
plugins: [ Ext.ux.FieldLabeller ],
fieldLabel: 'Subject',
name: 'subject'
}, {
plugins: [ Ext.ux.FieldLabeller ],
xtype: 'textarea',
labelAlign: 'top',
fieldLabel: 'Message text',
hideLabel: true,
name: 'msg',
flex: 1
}]
});

var window = new Ext.Window({
title: 'Resize Me',
width: 500,
height: 300,
minWidth: 300,
minHeight: 200,
layout: 'fit',
plain: true,
bodyStyle: 'padding:5px;',
buttonAlign: 'center',
items: form,
buttons: [{
text: 'Send'
},{
text: 'Cancel'
}]
});
window.show();
});

Condor
10 Aug 2009, 12:19 AM
Nice, but you are completely ignoring the labelAlign setting.

Animal
10 Aug 2009, 12:37 AM
<grumble...>

The impossible we do at once, miracles take a little longer...

;)

I'll see what I can come up with.

Animal
10 Aug 2009, 12:52 AM
OK, try this:



Ext.Container.prototype.bufferResize = false;

Ext.ux.FieldLabeller = (function(){

// Pulls a named property down from the first ancestor Container it's found in
function getParentProperty(propName) {
for (var p = this.ownerCt; p; p = p.ownerCt) {
if (p[propName]) {
return p[propName];
}
}
}

return {

// Add behaviour at important points in the Field's lifecycle.
init: function(f) {
f.onRender = f.onRender.createSequence(this.onRender);
f.onResize = f.onResize.createSequence(this.onResize);
f.onDestroy = f.onDestroy.createSequence(this.onDestroy);
},

onRender: function() {
// Do nothing if being rendered by a form layout
if (this.ownerCt) {
if (this.ownerCt.layout instanceof Ext.layout.FormLayout) {
return;
}
if (this.nextSibling()) {
this.margins = '0 0 5 0';
}
}

this.resizeEl = this.el.wrap({
cls: 'x-form-element'
});
this.positionEl = this.itemCt = this.resizeEl.wrap({
cls: 'x-form-item '
});
this.actionMode = 'itemCt';

// If we are hiding labels, then we're done!
if (!Ext.isDefined(this.hideLabels)) {
this.hideLabels = getParentProperty.call(this, "hideLabels");
}
if (this.hideLabels) {
this.resizeEl.setStyle('padding-left', '0px');
return;
}

// Collect info we need to render the label.
if (!Ext.isDefined(this.labelWidth)) {
this.labelWidth = getParentProperty.call(this, "labelWidth") || 100;
}
if (!Ext.isDefined(this.labelSeparator)) {
this.labelSeparator = getParentProperty.call(this, "labelSeparator");
}
if (!Ext.isDefined(this.labelPad)) {
this.labelPad = getParentProperty.call(this, "labelPad");
}
if (!Ext.isDefined(this.labelAlign)) {
this.labelAlign = getParentProperty.call(this, "labelAlign") || 'left';
}
this.itemCt.addClass('x-form-label-' + this.labelAlign);

if(this.labelAlign == 'top'){
this.labelWidth = 'auto';
this.resizeEl.setStyle('padding-left', '0px');
} else {
this.labelWidth += 'px';
this.resizeEl.setStyle('padding-left', (this.labelWidth + (this.labelPad || 5)) + 'px');
}

this.label = this.itemCt.insertFirst({
tag: 'label',
cls: 'x-form-item-label',
style: {
width: this.labelWidth
},
html: this.fieldLabel + (this.labelSeparator || ':')
});
},

// private
// Ensure the input field is sized to fit in the content area of the resizeEl (to the right of its padding-left)
onResize: function() {
this.el.setWidth(this.resizeEl.getWidth(true));
if (this.el.dom.tagName.toLowerCase() == 'textarea') {
var h = this.resizeEl.getHeight(true);
if (this.labelAlign == 'top') {
h -= this.label.getHeight();
}
this.el.setHeight(h);
}
},

// private
// Ensure that we clean up on destroy.
onDestroy: function() {
this.itemCt.remove();
}
};
})();

Ext.ux.FieldReplicator = {
init: function(f) {
f.replicator = this;
f.enableKeyEvents = true;
f.on('change', this.onChange, this);
f.onKeyDown = f.onKeyDown.createInterceptor(this.onKeyDown);
},

// If tabbing out and the change event will be fired, flag that
// the change handler must focus whatever Field ends up as the nextSibling.
onKeyDown: function(e) {
if ((e.getKey() == Ext.EventObject.TAB) && !e.shiftKey && (String(this.startValue) !== String(this.getValue()))) {
this.focusNext = true;
}
},

// Handle the field either being changed to blank or from blank.
onChange: function(f, n, o) {
var c = f.ownerCt, l,
ps = f.previousSibling(),
ns = f.nextSibling();
if (Ext.isEmpty(n)) {
if (!Ext.isEmpty(o)) {
// The Field has been blanked, and it is not the only one left, remove it
if ((ps && (ps.replicator === this)) || (ns && (ns.replicator === this))) {
l = f.findParentBy(function(p) {
return !Ext.isDefined(p.ownerCt);
});
c.remove(f);
l.doLayout();
}
}
} else {
if (Ext.isEmpty(o)) {
// Field filled, insert a clone as the next sibling
ns = new f.constructor(f.cloneConfig());
c.insert(c.items.indexOf(f) + 1, ns);
c.doLayout();
l = f.findParentBy(function(p) {
return !Ext.isDefined(p.ownerCt);
});
l.doLayout();
}
}
if (f.focusNext) {
delete f.focusNext;
ns.focus(false, true);
}
}
};

Ext.onReady(function() {
var form = new Ext.form.FormPanel({
baseCls: 'x-plain',
labelWidth: 55,
url:'save-form.php',
layout: {
type: 'vbox',
align: 'stretch'
},
defaults: {
xtype: 'textfield',
anchor: '100%' // anchor width by percentage
},

items: [{
plugins: [ Ext.ux.FieldReplicator, Ext.ux.FieldLabeller ],
fieldLabel: 'Send To',
name: 'to'
},{
plugins: [ Ext.ux.FieldLabeller ],
fieldLabel: 'Subject',
name: 'subject'
}, {
plugins: [ Ext.ux.FieldLabeller ],
xtype: 'textarea',
labelAlign: 'top',
fieldLabel: 'Message text',
hideLabel: true,
name: 'msg',
flex: 1
}]
});

var window = new Ext.Window({
title: 'Resize Me',
width: 500,
height: 300,
minWidth: 300,
minHeight: 200,
layout: 'fit',
plain: true,
bodyStyle: 'padding:5px;',
buttonAlign: 'center',
items: form,
buttons: [{
text: 'Send'
},{
text: 'Cancel'
}]
});
window.show();
});