Skip to content

Commit b3abc5f

Browse files
authored
feat(text): valueFormatter for easy and flexible input auto-formatting (#10264)
#10249
1 parent f549667 commit b3abc5f

6 files changed

Lines changed: 126 additions & 27 deletions

File tree

‎apps/toolbox/src/pages/forms.ts‎

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Page, Observable, EventData } from '@nativescript/core';
1+
import { Page, Observable, EventData, TextField, PropertyChangeData } from '@nativescript/core';
22

33
let page: Page;
44

@@ -9,8 +9,71 @@ export function navigatingTo(args: EventData) {
99

1010
export class SampleData extends Observable {
1111
textInput = '';
12-
textChange(args) {
13-
console.log(args.object.text);
14-
this.notifyPropertyChange('textInput', args.object.text);
12+
formattedSSNInput = '';
13+
formattedPhoneInput = '';
14+
valueFormatterSSN = formatSSN;
15+
valueFormatterPhone = formatPhoneNumber;
16+
17+
textChange(args: PropertyChangeData) {
18+
console.log(args.value);
19+
this.notifyPropertyChange('textInput', args.value);
20+
}
21+
22+
textChangeSSN(args: PropertyChangeData) {
23+
console.log(args.value);
24+
this.notifyPropertyChange('formattedSSNInput', args.value);
25+
}
26+
27+
textChangePhone(args: PropertyChangeData) {
28+
console.log(args.value);
29+
this.notifyPropertyChange('formattedPhoneInput', args.value);
30+
}
31+
}
32+
33+
function formatPhoneNumber(value: string, useParens?: boolean) {
34+
value = value.replace(/\D/g, '');
35+
var size = value.length;
36+
if (useParens) {
37+
if (size > 0) {
38+
value = '(' + value;
39+
}
40+
if (size > 3) {
41+
value = value.slice(0, 4) + ') ' + value.slice(4, 11);
42+
}
43+
if (size > 6) {
44+
value = value.slice(0, 9) + '-' + value.slice(9);
45+
}
46+
} else {
47+
if (size > 3) {
48+
value = value.slice(0, 3) + '-' + value.slice(3, 10);
49+
}
50+
if (size > 6) {
51+
value = value.slice(0, 7) + '-' + value.slice(7);
52+
}
1553
}
54+
return value;
55+
}
56+
57+
function formatSSN(value: string) {
58+
// if input value is falsy eg if the user deletes the input, then just return
59+
if (!value) return value;
60+
61+
// clean the input for any non-digit values
62+
const ssn = value.replace(/[^\d]/g, '');
63+
64+
// ssnLength is used to know when to apply our formatting for the ssn
65+
const ssnLength = ssn.length;
66+
67+
// we need to return the value with no formatting if its less then four digits
68+
if (ssnLength < 4) return ssn;
69+
70+
// if ssnLength is greater than 4 and less the 6 we start to return
71+
// the formatted number
72+
if (ssnLength < 6) {
73+
return `${ssn.slice(0, 3)}-${ssn.slice(3)}`;
74+
}
75+
76+
// finally, if the ssnLength is greater then 6, we add the last
77+
// bit of formatting and return it.
78+
return `${ssn.slice(0, 3)}-${ssn.slice(3, 5)}-${ssn.slice(5, 9)}`;
1679
}

‎apps/toolbox/src/pages/forms.xml‎

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
2-
<Page.actionBar>
3-
<ActionBar title="Labels and TextView" class="action-bar">
4-
</ActionBar>
5-
</Page.actionBar>
6-
<ScrollView>
7-
<StackLayout padding="20">
8-
<Label text="TextField:" fontWeight="bold" />
9-
<TextField textChange="{{ textChange }}" marginTop="6" backgroundColor="#efefef" padding="8" fontSize="18" keyboardType="url" />
10-
11-
<Label text="{{ textInput }}" marginTop="6" />
12-
13-
</StackLayout>
14-
</ScrollView>
2+
<Page.actionBar>
3+
<ActionBar title="Labels and TextView" class="action-bar">
4+
</ActionBar>
5+
</Page.actionBar>
6+
<ScrollView>
7+
<StackLayout padding="20">
8+
<Label text="TextField:" fontWeight="bold" />
9+
<TextField textChange="{{ textChange }}" marginTop="6" backgroundColor="#efefef" padding="8" fontSize="18" keyboardType="url" />
10+
11+
<Label text="{{ textInput }}" marginTop="6" />
12+
13+
<Label text="SSN Formatted input:" fontWeight="bold" marginTop="12" />
14+
<TextField hint="XXX-XX-XXXX" style.placeholderColor="silver" textChange="{{textChangeSSN}}" keyboardType="number" color="black" width="80%" borderColor="silver" borderWidth="1" height="40" textAlignment="center" borderRadius="4" valueFormatter="{{valueFormatterSSN}}" padding="5" maxLength="11">
15+
</TextField>
16+
<Label text="{{ formattedSSNInput }}" marginTop="6" />
17+
18+
<Label text="Phone Formatted input:" fontWeight="bold" marginTop="12" />
19+
<TextField hint="XXX-XXX-XXXX" style.placeholderColor="silver" textChange="{{textChangePhone}}" keyboardType="number" color="black" width="80%" borderColor="silver" borderWidth="1" height="40" textAlignment="center" borderRadius="4" valueFormatter="{{valueFormatterPhone}}" padding="5" maxLength="12">
20+
</TextField>
21+
<Label text="{{ formattedPhoneInput }}" marginTop="6" />
22+
23+
</StackLayout>
24+
</ScrollView>
1525
</Page>

‎packages/core/ui/editable-text-base/editable-text-base-common.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export abstract class EditableTextBase extends TextBase implements EditableTextB
2121
public autocorrect: boolean;
2222
public hint: string;
2323
public maxLength: number;
24+
public valueFormatter: (value: string) => string;
2425

2526
public abstract dismissSoftInput();
2627
public abstract _setInputType(inputType: number): void;

‎packages/core/ui/editable-text-base/index.android.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,10 @@ export abstract class EditableTextBase extends EditableTextBaseCommon {
493493

494494
public onTextChanged(text: string, start: number, before: number, count: number): void {
495495
// called by android.text.TextWatcher
496+
if (this.valueFormatter) {
497+
this.text = this.valueFormatter(text.toString());
498+
this.android.setSelection((this.text || '').length);
499+
}
496500
// const owner = this.owner;
497501
// let selectionStart = owner.android.getSelectionStart();
498502
// owner.android.removeTextChangedListener(owner._editTextListeners);

‎packages/core/ui/editable-text-base/index.d.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class EditableTextBase extends TextBase {
3636
/**
3737
* Gets or sets the autofill type.
3838
*/
39-
autofillType: CoreTypes.AutofillType;
39+
autofillType: CoreTypes.AutofillType;
4040

4141
/**
4242
* Gets or sets whether the instance is editable.
@@ -58,6 +58,12 @@ export class EditableTextBase extends TextBase {
5858
*/
5959
maxLength: number;
6060

61+
/**
62+
* Format input values
63+
* Note: useful for input masking/formatting
64+
*/
65+
valueFormatter: (value: string) => string;
66+
6167
/**
6268
* Hides the soft input method, ususally a soft keyboard.
6369
*/

‎packages/core/ui/text-field/index.ios.ts‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,16 +190,31 @@ export class TextField extends TextFieldBase {
190190
}
191191

192192
if (this.updateTextTrigger === 'textChanged') {
193-
// 1. secureTextEntry with firstEdit should not replace
194-
// 2. emoji's should not replace value
195-
// 3. convenient keyboard shortcuts should not replace value (eg, '.com')
196-
const shouldReplaceString = (textField.secureTextEntry && this.firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length);
197-
if (shouldReplaceString) {
198-
textProperty.nativeValueChange(this, replacementString);
193+
if (this.valueFormatter) {
194+
// format/replace
195+
let currentValue = textField.text;
196+
let nativeValueChange = `${textField.text}${replacementString}`;
197+
if (replacementString === '') {
198+
// clearing when empty
199+
nativeValueChange = currentValue.slice(0, delta);
200+
}
201+
202+
const formattedValue = this.valueFormatter(nativeValueChange);
203+
textField.text = formattedValue;
204+
textProperty.nativeValueChange(this, formattedValue);
205+
return false;
199206
} else {
200-
if (range.location <= textField.text.length) {
201-
const newText = NSString.stringWithString(textField.text).stringByReplacingCharactersInRangeWithString(range, replacementString);
202-
textProperty.nativeValueChange(this, newText);
207+
// 1. secureTextEntry with firstEdit should not replace
208+
// 2. emoji's should not replace value
209+
// 3. convenient keyboard shortcuts should not replace value (eg, '.com')
210+
const shouldReplaceString = (textField.secureTextEntry && this.firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length);
211+
if (shouldReplaceString) {
212+
textProperty.nativeValueChange(this, replacementString);
213+
} else {
214+
if (range.location <= textField.text.length) {
215+
const newText = NSString.stringWithString(textField.text).stringByReplacingCharactersInRangeWithString(range, replacementString);
216+
textProperty.nativeValueChange(this, newText);
217+
}
203218
}
204219
}
205220
}

0 commit comments

Comments
 (0)