8 changed files with 1930 additions and 2 deletions
@ -0,0 +1,51 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2022 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
|
|||
<form [formGroup]="phoneFormGroup"> |
|||
<div class="phone-input-container"> |
|||
<div class="flags-select-container" *ngIf="enableFlagsSelect"> |
|||
<span class="flag-container">{{ flagIcon }}</span> |
|||
<mat-select class="country-select" formControlName="country"> |
|||
<mat-option *ngFor="let country of allCountries" [value]="country.iso2"> |
|||
<span class="country-flag">{{country.flag}}</span> |
|||
<span>{{country.name + ' +' + country.dialCode }}</span> |
|||
</mat-option> |
|||
</mat-select> |
|||
</div> |
|||
<mat-form-field class="phone-input"> |
|||
<mat-label translate>phone-input.phone-input-label</mat-label> |
|||
<input |
|||
formControlName="phoneNumber" |
|||
type="tel" |
|||
matInput |
|||
[placeholder]="phonePlaceholder" |
|||
[pattern]="phoneNumberPattern" |
|||
autocomplete="off" |
|||
[required]="required"> |
|||
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('required')"> |
|||
{{ 'phone-input.phone-input-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('invalidPhoneNumber')"> |
|||
{{ 'phone-input.phone-input-validation' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('pattern')"> |
|||
{{ 'phone-input.phone-input-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,60 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host ::ng-deep { |
|||
|
|||
.phone-input-container { |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
.phone-input { |
|||
width: 100%; |
|||
} |
|||
} |
|||
|
|||
.flags-select-container { |
|||
display: inline-block; |
|||
position: relative; |
|||
width: 50px; |
|||
height: 100%; |
|||
margin-right: 5px; |
|||
} |
|||
|
|||
.flag-container { |
|||
position: absolute; |
|||
font-size: 20px; |
|||
top: 50%; |
|||
left: 0; |
|||
transform: translate(0, -50%); |
|||
} |
|||
.country-select { |
|||
width: 45px; |
|||
height: 30px; |
|||
|
|||
.country-flag { |
|||
font-size: 22px; |
|||
padding-right: 10px; |
|||
} |
|||
|
|||
.mat-select-trigger { |
|||
height: 100%; |
|||
width: 100%; |
|||
} |
|||
|
|||
.mat-select-value { |
|||
visibility: hidden; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,215 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
ValidatorFn, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Country, CountryData } from '@shared/models/country.models'; |
|||
import examples from 'libphonenumber-js/examples.mobile.json'; |
|||
import { CountryCode, getExampleNumber, parsePhoneNumberFromString } from 'libphonenumber-js'; |
|||
import { phoneNumberPattern } from '@shared/models/settings.models'; |
|||
import { Subscription } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-phone-input', |
|||
templateUrl: './phone-input.component.html', |
|||
styleUrls: ['./phone-input.component.scss'], |
|||
providers: [ |
|||
CountryData, |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => PhoneInputComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => PhoneInputComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class PhoneInputComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
@Input() disabled: boolean; |
|||
@Input() defaultCountry: CountryCode = 'US'; |
|||
@Input() enableFlagsSelect = true; |
|||
@Input() required = true; |
|||
|
|||
allCountries: Array<Country> = []; |
|||
phonePlaceholder: string; |
|||
countryCallingCode: string; |
|||
flagIcon: string; |
|||
|
|||
phoneNumberPattern = phoneNumberPattern; |
|||
|
|||
private modelValue: string; |
|||
public phoneFormGroup: FormGroup; |
|||
private valueChange$: Subscription = null; |
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private translate: TranslateService, |
|||
private fb: FormBuilder, |
|||
private countryCodeData: CountryData) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
if (this.enableFlagsSelect) { |
|||
this.fetchCountryData(); |
|||
} |
|||
this.phoneFormGroup = this.fb.group({ |
|||
country: [this.defaultCountry, []], |
|||
phoneNumber: [ |
|||
this.countryCallingCode, |
|||
this.required ? [Validators.required, Validators.pattern(phoneNumberPattern), this.validatePhoneNumber()] : [] |
|||
] |
|||
}); |
|||
|
|||
this.valueChange$ = this.phoneFormGroup.get('phoneNumber').valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
|
|||
this.phoneFormGroup.get('country').valueChanges.subscribe(value => { |
|||
if (value) { |
|||
const code = this.countryCallingCode; |
|||
this.getFlagAndPhoneNumberData(value); |
|||
let phoneNumber = this.phoneFormGroup.get('phoneNumber').value; |
|||
if (code !== this.countryCallingCode && phoneNumber) { |
|||
phoneNumber = phoneNumber.replace(code, this.countryCallingCode); |
|||
} else { |
|||
phoneNumber = this.countryCallingCode; |
|||
} |
|||
this.phoneFormGroup.get('phoneNumber').patchValue(phoneNumber); |
|||
} |
|||
}); |
|||
|
|||
this.phoneFormGroup.get('phoneNumber').valueChanges.subscribe(value => { |
|||
if (value) { |
|||
const parsedPhoneNumber = parsePhoneNumberFromString(value); |
|||
const country = this.phoneFormGroup.get('country').value; |
|||
if (parsedPhoneNumber?.country && parsedPhoneNumber?.country !== country) { |
|||
this.phoneFormGroup.get('country').patchValue(parsedPhoneNumber.country, {emitEvent: true}); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
if (this.valueChange$) { |
|||
this.valueChange$.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
getFlagAndPhoneNumberData(country) { |
|||
if (this.enableFlagsSelect) { |
|||
this.flagIcon = this.getFlagIcon(country); |
|||
} |
|||
this.getPhoneNumberData(country); |
|||
} |
|||
|
|||
getPhoneNumberData(country): void { |
|||
const phoneData = getExampleNumber(country, examples); |
|||
this.phonePlaceholder = phoneData.number; |
|||
this.countryCallingCode = '+' + phoneData.countryCallingCode; |
|||
} |
|||
|
|||
getFlagIcon(countryCode) { |
|||
const base = 127462 - 65; |
|||
return String.fromCodePoint(...countryCode.split('').map(country => base + country.charCodeAt(0))); |
|||
} |
|||
|
|||
validatePhoneNumber(): ValidatorFn { |
|||
return (c: FormControl) => { |
|||
const phoneNumber = c.value; |
|||
if (phoneNumber) { |
|||
const parsedPhoneNumber = parsePhoneNumberFromString(phoneNumber); |
|||
if (!parsedPhoneNumber?.isValid() || !parsedPhoneNumber?.isPossible()) { |
|||
return { |
|||
invalidPhoneNumber: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
return null; |
|||
}; |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.phoneFormGroup.get('phoneNumber').valid ? null : { |
|||
phoneFormGroup: false |
|||
}; |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.phoneFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.phoneFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
protected fetchCountryData(): void { |
|||
this.allCountries = []; |
|||
this.countryCodeData.allCountries.forEach((c) => { |
|||
const cc = c[1]; |
|||
const country: Country = { |
|||
name: c[0].toString(), |
|||
iso2: c[1].toString(), |
|||
dialCode: c[2].toString(), |
|||
flag: this.getFlagIcon(cc), |
|||
}; |
|||
|
|||
this.allCountries.push(country); |
|||
}); |
|||
} |
|||
|
|||
writeValue(phoneNumber): void { |
|||
this.modelValue = phoneNumber; |
|||
const country = phoneNumber ? parsePhoneNumberFromString(phoneNumber)?.country : this.defaultCountry; |
|||
this.getFlagAndPhoneNumberData(country); |
|||
this.phoneFormGroup.patchValue({phoneNumber, country}, {emitEvent: !phoneNumber}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const phoneNumber = this.phoneFormGroup.get('phoneNumber'); |
|||
if (phoneNumber.valid && this.modelValue !== phoneNumber.value) { |
|||
this.modelValue = phoneNumber.value; |
|||
this.propagateChange(this.modelValue); |
|||
} else { |
|||
this.propagateChange(null); |
|||
} |
|||
} |
|||
|
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue