UI: Phone input with country flag select
This commit is contained in:
parent
19a953428e
commit
baa681634b
@ -62,6 +62,7 @@
|
||||
"leaflet-providers": "^1.13.0",
|
||||
"leaflet.gridlayer.googlemutant": "^0.13.4",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"libphonenumber-js": "^1.10.4",
|
||||
"messageformat": "^2.3.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.34",
|
||||
|
||||
51
ui-ngx/src/app/shared/components/phone-input.component.html
Normal file
51
ui-ngx/src/app/shared/components/phone-input.component.html
Normal file
@ -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>
|
||||
60
ui-ngx/src/app/shared/components/phone-input.component.scss
Normal file
60
ui-ngx/src/app/shared/components/phone-input.component.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
215
ui-ngx/src/app/shared/components/phone-input.component.ts
Normal file
215
ui-ngx/src/app/shared/components/phone-input.component.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1587
ui-ngx/src/app/shared/models/country.models.ts
Normal file
1587
ui-ngx/src/app/shared/models/country.models.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -159,6 +159,7 @@ import { TbMarkdownComponent } from '@shared/components/markdown.component';
|
||||
import { ProtobufContentComponent } from '@shared/components/protobuf-content.component';
|
||||
import { CssComponent } from '@shared/components/css.component';
|
||||
import { SafePipe } from '@shared/pipe/safe.pipe';
|
||||
import { PhoneInputComponent } from '@shared/components/phone-input.component';
|
||||
|
||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||
return markedOptionsService;
|
||||
@ -277,7 +278,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
WidgetsBundleSearchComponent,
|
||||
CopyButtonComponent,
|
||||
TogglePasswordComponent,
|
||||
ProtobufContentComponent
|
||||
ProtobufContentComponent,
|
||||
PhoneInputComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -472,7 +474,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
WidgetsBundleSearchComponent,
|
||||
CopyButtonComponent,
|
||||
TogglePasswordComponent,
|
||||
ProtobufContentComponent
|
||||
ProtobufContentComponent,
|
||||
PhoneInputComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
||||
@ -3351,6 +3351,12 @@
|
||||
"material-icons": "Material icons",
|
||||
"show-all": "Show all icons"
|
||||
},
|
||||
"phone-input": {
|
||||
"phone-input-label": "Phone number",
|
||||
"phone-input-required": "Phone number is required",
|
||||
"phone-input-validation": "Phone number is invalid or not possible",
|
||||
"phone-input-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123."
|
||||
},
|
||||
"custom": {
|
||||
"widget-action": {
|
||||
"action-cell-button": "Action cell button",
|
||||
|
||||
@ -6027,6 +6027,11 @@ less@4.1.1:
|
||||
needle "^2.5.2"
|
||||
source-map "~0.6.0"
|
||||
|
||||
libphonenumber-js@^1.10.4:
|
||||
version "1.10.4"
|
||||
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.4.tgz#90397f0ed620262570a32244c9fbc389cc417ce4"
|
||||
integrity sha512-9QWxEk4GW5RDnFzt8UtyRENfFpAN8u7Sbf9wf32tcXY9tdtnz1dKHIBwW2Wnfx8ypXJb9zUnTpK9aQJ/B8AlnA==
|
||||
|
||||
license-webpack-plugin@2.3.20:
|
||||
version "2.3.20"
|
||||
resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.20.tgz#f51fb674ca31519dbedbe1c7aabc036e5a7f2858"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user