Browse Source

UI: Fixed autofill country autocomplete and improved validation

pull/14802/head
Vladyslav_Prykhodko 9 months ago
parent
commit
48c31e47b9
  1. 14
      ui-ngx/src/app/core/utils.ts
  2. 13
      ui-ngx/src/app/shared/components/country-autocomplete.component.html
  3. 27
      ui-ngx/src/app/shared/components/country-autocomplete.component.scss
  4. 39
      ui-ngx/src/app/shared/components/country-autocomplete.component.ts
  5. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

14
ui-ngx/src/app/core/utils.ts

@ -27,13 +27,14 @@ import { serverErrorCodesTranslations } from '@shared/models/constants';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import {
CompiledTbFunction,
compileTbFunction, GenericFunction,
compileTbFunction,
GenericFunction,
isNotEmptyTbFunction,
TbFunction
} from '@shared/models/js-function.models';
import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core';
import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
const varsRegex = /\${([^}]*)}/g;
const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
@ -998,3 +999,12 @@ export const validateEmail = (control: AbstractControl): ValidationErrors | null
return emailRegex.test(control.value) ? null : {email: true};
};
export const requireMatch = (): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (value && typeof value === 'string') {
return { requireMatch: { value: value } };
}
return null;
};
}

13
ui-ngx/src/app/shared/components/country-autocomplete.component.html

@ -15,13 +15,19 @@
limitations under the License.
-->
<mat-form-field [formGroup]="countryFormGroup" class="mat-block" subscriptSizing="{{subscriptSizing}}" [appearance]="appearance">
<mat-form-field [formGroup]="countryFormGroup" class="mat-block" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label>{{ labelText }}</mat-label>
@if (countryFormGroup.get('country').value?.flag) {
<span matPrefix>
{{ countryFormGroup.get('country').value.flag }}
</span>
}
<input matInput type="text"
#countryInput
#autocompleteTrigger="matAutocompleteTrigger"
formControlName="country"
(focusin)="onFocus()"
(blur)="onTouched()"
(blur)="checkInputAndAutoSelect()"
[required]="required"
[matAutocomplete]="countryAutocomplete">
<button *ngIf="countryFormGroup.get('country').value && !disabled"
@ -55,4 +61,7 @@
<mat-error *ngIf="countryFormGroup.get('country').hasError('required')">
{{ requiredText }}
</mat-error>
<mat-error *ngIf="countryFormGroup.get('country').hasError('requireMatch')">
{{ requiredMatchText }}
</mat-error>
</mat-form-field>

27
ui-ngx/src/app/shared/components/country-autocomplete.component.scss

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2026 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 {
.mat-form-field-appearance-fill {
.mat-mdc-form-field-icon-prefix {
align-self: baseline;
& > * {
color: initial;
padding-left: 8px;
}
}
}
}

39
ui-ngx/src/app/shared/components/country-autocomplete.component.ts

@ -23,14 +23,16 @@ import {
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
Validator,
Validators
} from '@angular/forms';
import { isNotEmptyStr } from '@core/utils';
import { isNotEmptyStr, requireMatch } from '@core/utils';
import { Observable, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
interface CountrySearchData extends Country {
searchText?: string;
@ -39,6 +41,7 @@ interface CountrySearchData extends Country {
@Component({
selector: 'tb-country-autocomplete',
templateUrl: 'country-autocomplete.component.html',
styleUrls: ['./country-autocomplete.component.scss'],
providers: [
CountryData,
{
@ -61,6 +64,9 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
@Input()
requiredText = this.translate.instant('contact.country-required');
@Input()
requiredMatchText = this.translate.instant('contact.country-required-match');
@Input()
autocompleteHint: string;
@ -79,6 +85,8 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
@ViewChild('countryInput', {static: true}) countryInput: ElementRef;
@ViewChild('autocompleteTrigger') autocompleteTrigger: MatAutocompleteTrigger;
@Output()
selectCountryCode = new EventEmitter<string>();
@ -103,11 +111,15 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
private countryData: CountryData,
private translate: TranslateService) {
this.countryFormGroup = this.fb.group({
country: ['']
country: ['', requireMatch()]
});
}
ngOnInit(): void {
if (this.required) {
this.countryFormGroup.get('country').addValidators(Validators.required);
this.countryFormGroup.get('country').updateValueAndValidity();
}
this.filteredCountries = this.countryFormGroup.get('country').valueChanges.pipe(
debounceTime(150),
tap(value => {
@ -163,7 +175,7 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
}
displayCountryFn(country?: Country): string | undefined {
return country ? `${country.flag} ${country.name}` : undefined;
return country ? country.name : undefined;
}
onFocus() {
@ -173,6 +185,23 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
}
}
checkInputAndAutoSelect() {
const control = this.countryFormGroup.get('country');
const value = control.value;
if (value && typeof value === 'string') {
const filterValue = value.trim().toLowerCase();
const foundCountry = this.allCountries.find(c => c.name.toLowerCase() === filterValue);
if (foundCountry) {
control.setValue(foundCountry);
this.autocompleteTrigger.closePanel();
}
}
this.onTouched();
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1194,6 +1194,7 @@
"contact": {
"country": "Country",
"country-required": "Country is required.",
"country-required-match": "Please select a valid country from the list.",
"city": "City",
"state": "State / Province",
"postal-code": "Zip / Postal Code",

Loading…
Cancel
Save