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-providers": "^1.13.0",
|
||||||
"leaflet.gridlayer.googlemutant": "^0.13.4",
|
"leaflet.gridlayer.googlemutant": "^0.13.4",
|
||||||
"leaflet.markercluster": "^1.5.3",
|
"leaflet.markercluster": "^1.5.3",
|
||||||
|
"libphonenumber-js": "^1.10.4",
|
||||||
"messageformat": "^2.3.0",
|
"messageformat": "^2.3.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"moment-timezone": "^0.5.34",
|
"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 { ProtobufContentComponent } from '@shared/components/protobuf-content.component';
|
||||||
import { CssComponent } from '@shared/components/css.component';
|
import { CssComponent } from '@shared/components/css.component';
|
||||||
import { SafePipe } from '@shared/pipe/safe.pipe';
|
import { SafePipe } from '@shared/pipe/safe.pipe';
|
||||||
|
import { PhoneInputComponent } from '@shared/components/phone-input.component';
|
||||||
|
|
||||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||||
return markedOptionsService;
|
return markedOptionsService;
|
||||||
@ -277,7 +278,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
WidgetsBundleSearchComponent,
|
WidgetsBundleSearchComponent,
|
||||||
CopyButtonComponent,
|
CopyButtonComponent,
|
||||||
TogglePasswordComponent,
|
TogglePasswordComponent,
|
||||||
ProtobufContentComponent
|
ProtobufContentComponent,
|
||||||
|
PhoneInputComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -472,7 +474,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
WidgetsBundleSearchComponent,
|
WidgetsBundleSearchComponent,
|
||||||
CopyButtonComponent,
|
CopyButtonComponent,
|
||||||
TogglePasswordComponent,
|
TogglePasswordComponent,
|
||||||
ProtobufContentComponent
|
ProtobufContentComponent,
|
||||||
|
PhoneInputComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule { }
|
export class SharedModule { }
|
||||||
|
|||||||
@ -3351,6 +3351,12 @@
|
|||||||
"material-icons": "Material icons",
|
"material-icons": "Material icons",
|
||||||
"show-all": "Show all 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": {
|
"custom": {
|
||||||
"widget-action": {
|
"widget-action": {
|
||||||
"action-cell-button": "Action cell button",
|
"action-cell-button": "Action cell button",
|
||||||
|
|||||||
@ -6027,6 +6027,11 @@ less@4.1.1:
|
|||||||
needle "^2.5.2"
|
needle "^2.5.2"
|
||||||
source-map "~0.6.0"
|
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:
|
license-webpack-plugin@2.3.20:
|
||||||
version "2.3.20"
|
version "2.3.20"
|
||||||
resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.20.tgz#f51fb674ca31519dbedbe1c7aabc036e5a7f2858"
|
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