Use renderer for ExtJS form field - extjs

I'm trying to create a PercentField component that extends textfield for use on ExtJS forms. The behavior I'm looking for is for the field to display percent values, e.g. 25% or 400%, but have the underlying value when the user is editing the field or the form is being submitted be a decimal, e.g. .25 or 4.
I've succeeded in getting this working by using a renderer in a grid column, (Here's a fiddle) but it doesn't look like textfield has a renderer property for using the field in basic forms. I've looked at the rawToValue and valueToRaw methods, but they don't seem to be quite what I'm looking for. Any advice?

As far as I know, there's no possibility of template for form fields. That would require to flip the input element and display a div or something, on focus and blur. That would be doable, but that implies some fine tuned CSS.
A simpler option is to implement custom valueToRaw and rawToValue methods, and let Ext handles the value lifecycle (which is really the complicated part). You'll still have to change the raw value on focus and blur, but that remains pretty straightforward.
Here's an example you can build upon (see fiddle):
Ext.define('My.PercentTextField', {
extend: 'Ext.form.field.Text',
onFocus: function() {
this.callParent(arguments);
var v = this.getValue();
if (Ext.isNumeric(v)) {
this.setRawValue(this.rawToValue(v));
}
},
onBlur: function() {
this.callParent(arguments);
var v = this.getValue();
if (Ext.isNumeric(v)) {
this.setRawValue(this.valueToRaw(v));
}
},
valueToRaw: function(v) {
return Ext.isEmpty(v)
? ''
: v * 100 + ' %';
},
rawToValue: function(v) {
// cast to float
if (!Ext.isEmpty(v)) {
var pcRe = /^(\d*(?:\.\d*)?)\s*%$/,
dcRe = /^\d*(?:\.\d*)?$/,
precision = 2,
floatValue,
match;
if (match = dcRe.test(v)) { // decimal input, eg. .33
floatValue = v * 1;
} else if (match = pcRe.exec(v)) { // % input, eg. 33 %
floatValue = match[1] / 100;
} else {
// invalid input
return undefined;
}
floatValue = Number.parseFloat(floatValue);
if (isNaN(floatValue)) {
return undefined;
} else {
return floatValue.toFixed(precision);
}
} else {
return undefined;
}
}
});

Related

Autocomplete off on Angular Directive for Date Picker

I have a directive for JQuery Date picker which injects date picker into input HTML control. This was developed by a previous developer and I am pretty new to Angular at this moment.
My question is that is there any way to prevent showing auto complete on all the date pickers that we inject via this directive?
export class DanialDatePickerDirective implements ControlValueAccessor {
constructor(protected el: ElementRef, private renderer: Renderer) { }
#Input() dateformat: string = "DD-MMM-YY";
#Input() ngModel: any;
#Input() setDefaultDate: boolean;
onModelChange: Function = () => { };
onModelTouched: Function = () => { };
writeValue(value: any) {
if (value) {
var ff = new Date(value);
$(this.el.nativeElement).datepicker("setDate", ff);
}
else {
$(this.el.nativeElement).datepicker("setDate", "");
}
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
onBlur() {
this.onModelTouched();
}
ngAfterViewInit() {
var self = this;
$(this.el.nativeElement).datepicker({
dateFormat: 'dd-M-y',
changeMonth: true,
changeYear: true,
showOtherMonths: true,
selectOtherMonths: true
});
if (this.setDefaultDate) {
var ff = new Date(self.ngModel);
setTimeout(function () {
$(self.el.nativeElement).datepicker("setDate", ff);
}, 200);
}
$(this.el.nativeElement).on('change', (e: any) => {
var model = e.target.value;
var date = null;
var monthstring = '';
if (model.indexOf("-") > 0){
monthstring = model.substring(model.indexOf("-") + 1, 5);
}
if (isNaN(parseInt(monthstring))) {
var tt = moment(model, "DD-MMM-YY").format('YYYY-MM-DD');
date = tt;
model = moment(model, "DD-MMM-YYYY").format('MM-DD-YYYY')
}
else {
date = moment(model, "DD-MM-YYYY").format('YYYY-MM-DD');
model = moment(model, "DD-MM-YYYY").format('MM-DD-YYYY')
}
$(".ui-datepicker a").removeAttr("href");
self.onModelChange(date);
self.writeValue(date.toString());
});
}
}
The only approach who works for me:
First, make sure to set autocomplete="off" on both, the input element itself and the parent form.
Second, make sure to assign an unique name to your input field always.
This can be achieved by simply generating a random number and using this number in the name of the field.
private getUniqueName() {
return Math.floor(Math.random() * Date.now());
}
Explanation:
In the past, many developers would add autocomplete="off" to their
form fields to prevent the browser from performing any kind of
autocomplete functionality. While Chrome will still respect this tag
for autocomplete data, it will not respect it for autofill data.
https://developers.google.com/web/updates/2015/06/checkout-faster-with-autofill.
So autocomplete="off" solves the autocomplete issue. But to solve the autofill you need to play dirty with the browser by changing the name of the input over an over again, that way the browser will never know how to autofill ;)

ExtJS - Grid filter with multiple columns and multiple values

I create a grid and a toolbar with Two Menu of menuCheckItem. When i check the menuCheckItem the grid filters even with multiple values and multiple columns.
This working fine, as I have created grid 1st and then the toolbar
this.up('') // Used Instead of Ext.getCmp()
Working FIDDLE - https://fiddle.sencha.com/#view/editor&fiddle/2lop
Now I am trying to create same toolbar along with Menu separately on top 1st and then create grid at below. But while doing this, nly Multiple values is working.
I am trying to filter grid with multiple values as well as multiple columns.
Few things i tried -
// Only Filters One Value at a time with each Columns
store.queryBy(function(record,id){
return (record.get('name') == someValue && record.get('phone') == otherValue);
});
and
// Filters Many Columns with Single Value
filter.add(
property : name, phone
value : "somevalue"
operator : "OR"
);
Is there any way to implement Toolbar 1st and then grid ? And Filter grid with many values and columns simultaneously ?
In this FIDDLE i remade a function(checkchange), which is universal , can be put separately and you'll be able to attach it to every menucheckitem you create. The only thing is that if you add new menucheckitem filter you should name the menucheckitem id with the name of the columnDataIndex-Menu and add this columnDataIndex in menuFilters and thats all.
checkchange: function (checkbox, checked, eOpts) {
var menuFilters = ['name', 'phone'];
var getChecked = function (m) {
var checkedItems = [];
m.items.items.forEach(function (c) {
if (c.checked) {
checkedItems.push(c.text);
}
});
return checkedItems;
};
//
var menus = new Map();
menuFilters.forEach(function (e) {
menus.set(e, Ext.getCmp(e + '-Menu'));
});
//
var fieldValues = [];
menuFilters.forEach(function (e) {
fieldValues.push([e, getChecked(menus.get(e))]);
});
//
var store = checkbox.up('grid').store;
store.clearFilter();
//
if (fieldValues.length > 0) {
store.filterBy(function (record) {
var fV = this.fieldValues;
for (var i = 0; i < fV.length; i++) {
if (fV[i][1].length > 0) {
if (fV[i][1].indexOf(record.get(fV[i][0])) === -1) {
return false;
}
}
}
return true;
}, {
fieldValues: fieldValues
});
}
}

Fieldcontainer incorrectly displays in toolbar with label aligned top

FieldContainer doesn't show correctly when you put labelAlign: 'top'.
Find my Fiddle : https://fiddle.sencha.com/#fiddle/1c2s
I create a custom field which is base on field container.
If you resize the window to a smaller size, you'll see that the textfield will go above the fieldContiner.
Any idea on how to fix this? Any workaround?
I've tried several stuff but I'm struggling... I don't now where I can act to change this...
And I definitly need to fix this.
Thanks in advance
(for reference: Open bug in Sencha forum: https://www.sencha.com/forum/showthread.php?311212-Fieldcontainer-incorrectly-displays-in-toolbar-with-label-aligned-top)
Looks like bug indeed, but there is pretty easy way to make that work - just set minHeight: 65 to your url field.
Fiddle: https://fiddle.sencha.com/#fiddle/1caa
I'm pretty sur I found the answer.
There is a mismatch between the build src and the doc!
If you check in your built source for file src/layout/component/field/FieldContainer.js you will notice that it doesn't correspond to the doc (and especially some method missing like calculateOwnerHeightFromContentHeight).
note: This has been fixed in ExtJs 6.2.0
Proposed override to fix this:
/**
* Override to fix componentLayout wrong calculation of height when labelAlign='top'
*
* See post forum:
* {#link https://www.sencha.com/forum/showthread.php?311212-Fieldcontainer-incorrectly-displays-in-toolbar-with-label-aligned-top}
*/
Ext.define('MyApp.overrides.layout.component.field.FieldContainer', {
override: 'Ext.layout.component.field.FieldContainer',
compatibility: [
'6.0.0',
'6.0.1',
'6.0.2'
],
/* Begin Definitions */
calculateOwnerHeightFromContentHeight: function(ownerContext, contentHeight) {
var h = this.callSuper([ownerContext, contentHeight]);
return h + this.getHeightAdjustment();
},
calculateOwnerWidthFromContentWidth: function(ownerContext, contentWidth) {
var w = this.callSuper([ownerContext, contentWidth]);
return w + this.getWidthAdjustment();
},
measureContentHeight: function(ownerContext) {
// since we are measuring the outer el, we have to wait for whatever is in our
// container to be flushed to the DOM... especially for things like box layouts
// that size the innerCt since that is all that will contribute to our size!
return ownerContext.hasDomProp('containerLayoutDone') ? this.callSuper([ownerContext]) : NaN;
},
measureContentWidth: function(ownerContext) {
// see measureContentHeight
return ownerContext.hasDomProp('containerLayoutDone') ? this.callSuper([ownerContext]) : NaN;
},
publishInnerHeight: function(ownerContext, height) {
height -= this.getHeightAdjustment();
ownerContext.containerElContext.setHeight(height);
},
publishInnerWidth: function(ownerContext, width) {
width -= this.getWidthAdjustment();
ownerContext.containerElContext.setWidth(width);
},
privates: {
getHeightAdjustment: function() {
var owner = this.owner,
h = 0;
if (owner.labelAlign === 'top' && owner.hasVisibleLabel()) {
h += owner.labelEl.getHeight();
}
if (owner.msgTarget === 'under' && owner.hasActiveError()) {
h += owner.errorWrapEl.getHeight();
}
return h + owner.bodyEl.getPadding('tb');
},
getWidthAdjustment: function() {
var owner = this.owner,
w = 0;
if (owner.labelAlign !== 'top' && owner.hasVisibleLabel()) {
w += (owner.labelWidth + (owner.labelPad || 0));
}
if (owner.msgTarget === 'side' && owner.hasActiveError()) {
w += owner.errorWrapEl.getWidth();
}
return w + owner.bodyEl.getPadding('lr');
}
}
});

How to remove the summary grouping of particular row in EXTJS

If I apply groupField property in the grid and store, extjs will automatically sort records by groupField.
For example: http://dev.sencha.com/deploy/ext-4.0.0/examples/grid/group-summary-grid.html, it used 'project' as groupField.
My Question is How can I add an extra row, which does not belong to any group, into a grid with groupField. (Like this: http://i59.tinypic.com/30s973l.jpg)
By default group is made for each value from groupField, also for empty groupField (but it appears first). If it is enough for you then you don't have to do anything.
Fiddle: http://jsfiddle.net/tme462tj/
When you want to have behavior like on attached image, then some work has to be done.
To put empty value at the end, you can create convert function for model field (eg: convert: function(v) { return v ? v : defaultDepartment; }). If you want to hide header and summary for this empty group, you can attach event listener to viewready event and hide them using styles:
viewready: function() {
var els = this.getEl().query('.x-grid-group-hd');
els = Ext.Array.filter(els, function(e) { return e.textContent.trim() == defaultDepartment; });
if (els.length !== 1) {
return;
}
var header = Ext.fly(els[0]);
header.setStyle('display', 'none');
var summary = header.up('.x-group-hd-container').down('.x-grid-row-summary');
if (summary) {
summary.setStyle('display', 'none');
}
}
Fiddle: http://jsfiddle.net/hdyokgnx/1/
As per my comment on the accepted solution, this revision makes the grouping header hide permanently for the duration of that grid view.
This solves my problem of needing a grouping grid but with one group that does not have a group header row.
store.load
({
params :
{
arg1 : 1
,arg2 : '2'
}
,callback : function( records, operation, success )
{
var els = me.theGrid.getEl().query('.x-grid-group-hd');
els = Ext.Array.filter( els, function(e)
{
return e.textContent.trim().includes( "*** A SUBSTRING IN GROUPING FIELD ***" );
});
if (els.length !== 1)
{
return;
}
var header = Ext.fly(els[0]);
header.setStyle('display', 'none');
var summary = header.up('.x-group-hd-container').down('.x-grid-row-summary');
if (summary)
{
summary.setStyle('display', 'none');
}
}
});

Using AngularJS directive to format input field while leaving scope variable unchanged

I'm having an issue formatting an input field, while leaving the underlying scope variable non-formatted.
What I want to achieve is a text field to display currency. It should format itself on the fly, while handling wrong input. I got that working, but my problem is that I want to store the non-formatted value in my scope variable. The issue with input is that it requires a model which goes both ways, so changing the input field updates the model, and the other way around.
I came upon $parsers and $formatters which appears to be what I am looking for. Unfortunately they are not affecting each other (which might actually be good to avoid endless loops).
I've created a simple jsFiddle: http://jsfiddle.net/cruckie/yE8Yj/ and the code is as follows:
HTML:
<div data-ng-app="app" data-ng-controller="Ctrl">
<input type="text" data-currency="" data-ng-model="data" />
<div>Model: {{data}}</div>
</div>
JS:
var app = angular.module("app", []);
function Ctrl($scope) {
$scope.data = 1234567;
}
app.directive('currency', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attr, ctrl) {
ctrl.$formatters.push(function(modelValue) {
return modelValue.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ',');
});
ctrl.$parsers.push(function(viewValue) {
return parseFloat(viewValue.replace(new RegExp(",", "g"), ''));
});
}
};
});
Again, this is just a simple example. When it loads everything looks as it's supposed to. The input field is formatted and the variable is not. However, when changing the value in the input field it no longer formats itself - the variable however gets updated correctly.
Is there a way to ensure the text field being formatted while the variable is not? I guess what I am looking for is a filter for text fields, but I can't seen to find anything on that.
Best regards
Here's a fiddle that shows how I implemented the exact same behavior in my application. I ended up using ngModelController#render instead of $formatters, and then adding a separate set of behavior that triggered on keydown and change events.
http://jsfiddle.net/KPeBD/2/
I've revised a little what Wade Tandy had done, and added support for several features:
thousands separator is taken from $locale
number of digits after decimal points is taken by default from $locale, and can be overridden by fraction attribute
parser is activated only on change, and not on keydown, cut and paste, to avoid sending the cursor to the end of the input on every change
Home and End keys are also allowed (to select the entire text using keyboard)
set validity to false when input is not numeric, this is done in the parser:
// This runs when we update the text field
ngModelCtrl.$parsers.push(function(viewValue) {
var newVal = viewValue.replace(replaceRegex, '');
var newValAsNumber = newVal * 1;
// check if new value is numeric, and set control validity
if (isNaN(newValAsNumber)){
ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', false);
}
else{
newVal = newValAsNumber.toFixed(fraction);
ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', true);
}
return newVal;
});
You can see my revised version here - http://jsfiddle.net/KPeBD/64/
I have refactored the original directive, so that it uses $parses and $formatters instead of listening to keyboard events. There is also no need to use $browser.defer
See working demo here http://jsfiddle.net/davidvotrubec/ebuqo6Lm/
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope) {
$scope.numericValue = 12345678;
});
//Written by David Votrubec from ST-Software.com
//Inspired by http://jsfiddle.net/KPeBD/2/
myApp.directive('sgNumberInput', ['$filter', '$locale', function ($filter, $locale) {
return {
require: 'ngModel',
restrict: "A",
link: function ($scope, element, attrs, ctrl) {
var fractionSize = parseInt(attrs['fractionSize']) || 0;
var numberFilter = $filter('number');
//format the view value
ctrl.$formatters.push(function (modelValue) {
var retVal = numberFilter(modelValue, fractionSize);
var isValid = isNaN(modelValue) == false;
ctrl.$setValidity(attrs.name, isValid);
return retVal;
});
//parse user's input
ctrl.$parsers.push(function (viewValue) {
var caretPosition = getCaretPosition(element[0]), nonNumericCount = countNonNumericChars(viewValue);
viewValue = viewValue || '';
//Replace all possible group separators
var trimmedValue = viewValue.trim().replace(/,/g, '').replace(/`/g, '').replace(/'/g, '').replace(/\u00a0/g, '').replace(/ /g, '');
//If numericValue contains more decimal places than is allowed by fractionSize, then numberFilter would round the value up
//Thus 123.109 would become 123.11
//We do not want that, therefore I strip the extra decimal numbers
var separator = $locale.NUMBER_FORMATS.DECIMAL_SEP;
var arr = trimmedValue.split(separator);
var decimalPlaces = arr[1];
if (decimalPlaces != null && decimalPlaces.length > fractionSize) {
//Trim extra decimal places
decimalPlaces = decimalPlaces.substring(0, fractionSize);
trimmedValue = arr[0] + separator + decimalPlaces;
}
var numericValue = parseFloat(trimmedValue);
var isEmpty = numericValue == null || viewValue.trim() === "";
var isRequired = attrs.required || false;
var isValid = true;
if (isEmpty && isRequired) {
isValid = false;
}
if (isEmpty == false && isNaN(numericValue)) {
isValid = false;
}
ctrl.$setValidity(attrs.name, isValid);
if (isNaN(numericValue) == false && isValid) {
var newViewValue = numberFilter(numericValue, fractionSize);
element.val(newViewValue);
var newNonNumbericCount = countNonNumericChars(newViewValue);
var diff = newNonNumbericCount - nonNumericCount;
var newCaretPosition = caretPosition + diff;
if (nonNumericCount == 0 && newCaretPosition > 0) {
newCaretPosition--;
}
setCaretPosition(element[0], newCaretPosition);
}
return isNaN(numericValue) == false ? numericValue : null;
});
} //end of link function
};
//#region helper methods
function getCaretPosition(inputField) {
// Initialize
var position = 0;
// IE Support
if (document.selection) {
inputField.focus();
// To get cursor position, get empty selection range
var emptySelection = document.selection.createRange();
// Move selection start to 0 position
emptySelection.moveStart('character', -inputField.value.length);
// The caret position is selection length
position = emptySelection.text.length;
}
else if (inputField.selectionStart || inputField.selectionStart == 0) {
position = inputField.selectionStart;
}
return position;
}
function setCaretPosition(inputElement, position) {
if (inputElement.createTextRange) {
var range = inputElement.createTextRange();
range.move('character', position);
range.select();
}
else {
if (inputElement.selectionStart) {
inputElement.focus();
inputElement.setSelectionRange(position, position);
}
else {
inputElement.focus();
}
}
}
function countNonNumericChars(value) {
return (value.match(/[^a-z0-9]/gi) || []).length;
}
//#endregion helper methods
}]);
Github code is here [https://github.com/ST-Software/STAngular/blob/master/src/directives/SgNumberInput]
Indeed the $parsers and $formatters are "independent" as you say (probably for loops, again as you say). In our application we explicitly format with the onchange event (inside the link function), roughly as:
element.bind("change", function() {
...
var formattedModel = format(ctrl.$modelValue);
...
element.val(formattedModel);
});
See your updated fiddle for the detailed and working example: http://jsfiddle.net/yE8Yj/1/
I like binding to the onchange event, because I find it annoying to change the input as the user is typing.
The fiddle is using an old version of angular(1.0.7).
On updating to a recent version, 1.2.6, the $render function of the ngModelCtrl is never called, meaning if the model value is changed in the controller,
the number is never formatted as required in the view.
//check if new value is numeric, and set control validity
if (isNaN(newValAsNumber)){
ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', false);
}
Here is the updated fiddle http://jsfiddle.net/KPeBD/78/
Based on the answer from Wade Tandy here is a new jsfiddle with following improvements:
decimal numbers possible
thousands separator and decimal separator based on locals
and some other tweaks ...
I have also replaces all String.replace(regex) by split().join(), because this allows me to use variables in the expression.
http://jsfiddle.net/KPeBD/283/

Resources