Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for (XXX) XXX-XXXX format and ability to remove country code #61

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
10 changes: 10 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,15 @@ <h4>Custom error message, auto-validates on blur</h4>
<div class="vertical-section">
<gold-phone-input auto-validate label="Cats only" error-message="needs more cats" required></gold-phone-input>
</div>

<h4>No country code, parentheses US format</h4>
<div class="vertical-section">
<gold-phone-input
no-country-code
phone-number-pattern="(XXX) XXX-XXXX"
label="No country code"
class="form-element">
</gold-phone-input>
</div>
</div>
</body>
121 changes: 107 additions & 14 deletions gold-phone-input.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@

By default, the phone number is considered to be a US phone number, and
will be validated according to the pattern `XXX-XXX-XXXX`, where `X` is a
digit, and `-` is the separating dash. If you want to customize the input
digit, and `-` is the separating dash. Formatting also supports single spaces. If you want to customize the input
for a different area code or number pattern, use the `country-code` and
`phone-number-pattern` attributes
`phone-number-pattern` attributes.

<gold-phone-input
country-code="33"
Expand Down Expand Up @@ -77,6 +77,10 @@
@apply(--gold-phone-input-country-code);
}

.label-no-country-code {
left: 0 !important;
}

.container {
@apply(--layout-horizontal);
}
Expand All @@ -101,15 +105,16 @@
<label hidden$="[[!label]]">[[label]]</label>

<div class="container">
<span class="country-code">+[[countryCode]]</span>


<span hidden$="[[noCountryCode]]" class="country-code">+[[countryCode]]</span>

<input is="iron-input" id="input"
aria-labelledby$="[[_ariaLabelledBy]]"
aria-describedby$="[[_ariaDescribedBy]]"
required$="[[required]]"
bind-value="{{value}}"
name$="[[name]]"
allowed-pattern="[0-9\-]"
allowed-pattern="[()\s0-9\-]"
autocomplete="tel"
type="tel"
prevent-invalid-input
Expand Down Expand Up @@ -157,6 +162,14 @@
value: '1'
},

/*
* Set this to true to disable the country code
*/
noCountryCode: {
type: Boolean,
value: false
},

/*
* The format of a valid phone number, including formatting but excluding
* the country code. Use 'X' to denote the digits separated by dashes.
Expand All @@ -181,51 +194,122 @@
// If there's an initial input, validate it.
if (this.value)
this._handleAutoValidate();

},

attached: function() {

if(this.noCountryCode) {
this.set('countryCode', '');
Polymer.dom(this).node.querySelector('label').classList.add('label-no-country-code');
}

this.set('maxlength', this.phoneNumberPattern.length);

},

_phoneNumberPatternChanged: function() {
// Transform the pattern into a regex the iron-input understands.
var regex = '';
regex = this.phoneNumberPattern.replace(/\s/g, '\\s');
regex = regex.replace(/\(/gi, '\\(');
regex = regex.replace(/\)/gi, '\\)');
regex = regex.replace(/X/gi, '\\d');
regex = regex.replace(/\+/g, '\\+');
this.$.input.pattern = regex;

this.regex = regex;
},

/**
* A handler that is called on input
*/
_onValueChanged: function(value, oldValue) {

// The initial property assignment is handled by `ready`.
if (oldValue == undefined || value === oldValue)
if (oldValue == undefined || value == undefined) {
this.value = '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "setting the value to undefined resets it to the empty string" test fails without this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But isn't the testing passing on master, without this change?

return;
}

//Ensure value is a string
value = value ? value.toString() : '';

// Keep track of how many dashes the original value has. After
// reformatting the value, we might gain or lose some of them, which
// means we have to correctly move the caret to account for the difference.
var start = this.$.input.selectionStart;

var initialDashesBeforeCaret = value.substr(0, start).split('-').length - 1;
var initialSpacesBeforeCaret = value.substr(0, start).split(' ').length - 1;
var initialLeftParenBeforeCaret = value.substr(0, start).split('(').length - 1;
var initialRightParenBeforeCaret = value.substr(0, start).split(')').length - 1;

var formattedValue = '';

// Remove any already-applied formatting.
value = value.replace(/-/g, '');
var shouldFormat = value.length <= this.phoneNumberPattern.replace(/-/g, '').length;
var formattedValue = '';
value = value.replace(/\(/g, '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of this would be enormously simplified if you force the format, including the country code to be xxx-xxxx-xxx, without the brackets. This isn't that uncommon to be honest, and it would add no code changes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This normalization is needed if we're going to support parentheses and spaces

Copy link
Contributor

@notwaldorf notwaldorf Jun 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but this code if very repetitive, which makes it brittle, which makes it hard to maintain. I would much rather lose formatting flexibility than have everything copy pasted 3 times :(

Can we somehow generalize the code? Maybe we don't need to count all the different kinds of characters added?

value = value.replace(/\)/g, '');
value = value.replace(/\s/g, '');

oldValue = oldValue.replace(/-/g, '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to do this twice, for both value and oldValue?

oldValue = oldValue.replace(/\(/g, '');
oldValue = oldValue.replace(/\)/g, '');
oldValue = oldValue.replace(/\s/g, '');

if(value === oldValue) {
return;
}

if(value.length > 10) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 is completely arbitrary, and doesn't work for numbers that aren't US phone numbers. The previous code calculated this based on the pattern

this.updateValueAndPreserveCaret(value.trim());
this._handleAutoValidate();
return;
}

// Fill in the dashes according to the specified pattern.
var leftParenIndex = this.phoneNumberPattern.indexOf('(');
var leftParenAdded = false;

var rightParenIndex = this.phoneNumberPattern.indexOf(')') - 1;
var rightParenAdded = false;

var currentSpaceIndex = 0;
var currentDashIndex = 0;
var totalDashesAdded = 0;

var totalCharactersAdded = 0;

for (var i = 0; i < value.length; i++) {

currentDashIndex = this.phoneNumberPattern.indexOf('-', currentDashIndex);

// Since we remove any formatting first, we need to account added dashes
currentSpaceIndex = this.phoneNumberPattern.indexOf(' ', currentSpaceIndex);

// Since we remove any formatting first, we need to account added dashes, etc.
// when counting the position of new dashes in the pattern.
if (shouldFormat && i == (currentDashIndex - totalDashesAdded)) {

if (i == leftParenIndex) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be abstracted in a function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be abstracted in a function?

formattedValue += '(';
leftParenAdded = true;
totalCharactersAdded++;
}

if (i == rightParenIndex) {
formattedValue += ')';
rightParenAdded = true;
totalCharactersAdded++;
}

if (i == (currentSpaceIndex - totalCharactersAdded)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a lot of comments. At the moment, it's very hard to understand what this code is doing

formattedValue += ' ';
currentSpaceIndex++;
totalCharactersAdded++;
}

if (i == (currentDashIndex - totalCharactersAdded)) {
formattedValue += '-';
currentDashIndex++;
totalDashesAdded++;
totalCharactersAdded++;
}

formattedValue += value[i];
Expand All @@ -234,12 +318,21 @@
var updatedDashesBeforeCaret = formattedValue.substr(0, start).split('-').length - 1;
var dashesDifference = updatedDashesBeforeCaret - initialDashesBeforeCaret;

var updatedSpacesBeforeCaret = formattedValue.substr(0, start+1).split(' ').length - 1;
var spacesDifference = updatedSpacesBeforeCaret - initialSpacesBeforeCaret;

var updatedLeftParenBeforeCaret = formattedValue.substr(0, start).split('(').length - 1;
var leftParenDifference = updatedLeftParenBeforeCaret - initialLeftParenBeforeCaret;

var updatedRightParenBeforeCaret = formattedValue.substr(0, start).split(')').length - 1;
var rightParenDifference = updatedRightParenBeforeCaret - initialRightParenBeforeCaret;

// Note: this will call _onValueChanged again, which will move the
// cursor to the end of the value. Correctly adjust the caret afterwards.
this.updateValueAndPreserveCaret(formattedValue.trim());

// Advance or back up the caret based on the change that happened before it.
this.$.input.selectionStart = this.$.input.selectionEnd = start + dashesDifference;
this.$.input.selectionStart = this.$.input.selectionEnd = start + spacesDifference + dashesDifference + leftParenDifference + rightParenDifference;

this._handleAutoValidate();
},
Expand Down
15 changes: 15 additions & 0 deletions test/basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@
</template>
</test-fixture>

<test-fixture id="parensFormat">
<template>
<gold-phone-input
phone-number-pattern="(XXX) XXX-XXXX"
></gold-phone-input>
</template>
</test-fixture>

<script>

suite('basic', function() {
Expand Down Expand Up @@ -101,6 +109,13 @@
assert.notEqual(getComputedStyle(error).visibility, 'hidden', 'error is not visibility:hidden');
assert.isTrue(container.invalid);
});

test('input format respects parentheses and spaces', function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests for incomplete matching too. Like, if I start with 11111, does it get formatted to (111) 11

var input = fixture('parensFormat');
input.value ='1231112222';
assert.equal(input.value, '(123) 111-2222');
});

});

suite('a11y', function() {
Expand Down