some ui fix and debug input field, separate price field
This commit is contained in:
@@ -17,48 +17,28 @@
|
||||
<p-toggleSwitch [attr.disabled]="disabled" [formControl]="control" />
|
||||
} @else {
|
||||
<p-inputgroup>
|
||||
<!-- [minlength]="minlength || null" -->
|
||||
@if (type === 'price') {
|
||||
<p-inputnumber
|
||||
[attr.id]="name"
|
||||
[formControl]="control"
|
||||
[maxlength]="preparedMaxLength || null"
|
||||
[attr.name]="name"
|
||||
[attr.placeholder]="preparedPlaceholder"
|
||||
[attr.type]="htmlType"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[attr.disabled]="disabled"
|
||||
[class]="`w-full ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
|
||||
[required]="isRequired"
|
||||
[invalid]="control.invalid && (control.touched || control.dirty)"
|
||||
(onInput)="onPriceInput($event)"
|
||||
(onBlur)="blur.emit()" />
|
||||
<!-- (input)="onInput($event)" -->
|
||||
} @else {
|
||||
<input
|
||||
pInputText
|
||||
[attr.id]="name"
|
||||
[formControl]="control"
|
||||
[attr.name]="name"
|
||||
[attr.placeholder]="preparedPlaceholder"
|
||||
[attr.inputmode]="inputMode"
|
||||
[attr.maxlength]="preparedMaxLength || null"
|
||||
[attr.type]="htmlType"
|
||||
[attr.disabled]="disabled"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[classList]="
|
||||
[
|
||||
inputClass,
|
||||
'w-full',
|
||||
`${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`,
|
||||
suffixTemp || preparedSuffix() ? 'hasSuffix' : '',
|
||||
].join(' ')
|
||||
"
|
||||
[required]="isRequired"
|
||||
[invalid]="control.invalid && (control.touched || control.dirty)"
|
||||
(onBlur)="blur.emit()"
|
||||
(input)="onInput($event)" />
|
||||
}
|
||||
<input
|
||||
pInputText
|
||||
[attr.id]="name"
|
||||
[formControl]="control"
|
||||
[attr.name]="name"
|
||||
[attr.placeholder]="preparedPlaceholder"
|
||||
[attr.inputmode]="inputMode"
|
||||
[attr.maxlength]="preparedMaxLength || null"
|
||||
[attr.disabled]="disabled"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[classList]="
|
||||
[
|
||||
inputClass,
|
||||
'w-full',
|
||||
`${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`,
|
||||
suffixTemp || preparedSuffix() ? 'hasSuffix' : '',
|
||||
].join(' ')
|
||||
"
|
||||
[required]="isRequired"
|
||||
[invalid]="control.invalid && (control.touched || control.dirty)"
|
||||
(onBlur)="blur.emit()"
|
||||
(input)="onInput($event)" />
|
||||
@if (suffixTemp) {
|
||||
<ng-container [ngTemplateOutlet]="suffixTemp" />
|
||||
} @else if (preparedSuffix()) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { InputGroupModule } from 'primeng/inputgroup';
|
||||
import { InputGroupAddon } from 'primeng/inputgroupaddon';
|
||||
import { InputMaskModule } from 'primeng/inputmask';
|
||||
import { InputNumberInputEvent, InputNumberModule } from 'primeng/inputnumber';
|
||||
import { InputNumberModule } from 'primeng/inputnumber';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { KeyFilterModule } from 'primeng/keyfilter';
|
||||
import { ToggleSwitch } from 'primeng/toggleswitch';
|
||||
@@ -51,8 +51,7 @@ export class InputComponent {
|
||||
| 'email'
|
||||
| 'checkbox'
|
||||
| 'switch'
|
||||
| 'number'
|
||||
| 'price' = 'simple';
|
||||
| 'number' = 'simple';
|
||||
@Input() control!: FormControl<Maybe<any>>;
|
||||
@Input() name!: string;
|
||||
@Input() label: string = '';
|
||||
@@ -80,17 +79,8 @@ export class InputComponent {
|
||||
|
||||
private readonly toastService = inject(ToastService);
|
||||
|
||||
// onInput(ev: Event) {
|
||||
// const v = (ev.target as HTMLInputElement).value;
|
||||
|
||||
// const newLocal = v || v == '0';
|
||||
// // this.valueChange.emit(this.type === 'price' && newLocal ? parseFloat(v) : v);
|
||||
// }
|
||||
|
||||
preparedSuffix = computed(() => {
|
||||
if (this.type === 'price') {
|
||||
return 'ریال';
|
||||
} else return this.suffix;
|
||||
return this.suffix;
|
||||
});
|
||||
|
||||
get preparedPlaceholder(): string | null {
|
||||
@@ -106,30 +96,34 @@ export class InputComponent {
|
||||
return 'شماره تلفن';
|
||||
case 'email':
|
||||
return 'آدرس ایمیل خود را وارد کنید';
|
||||
case 'price':
|
||||
return 'مبلغ';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
get inputMode(): string | null {
|
||||
get inputMode(): string {
|
||||
switch (this.type) {
|
||||
case 'price':
|
||||
return 'decimal';
|
||||
case 'number':
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
return 'decimal';
|
||||
case 'postalCode':
|
||||
case 'nationalId':
|
||||
return 'numeric';
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
return 'tel';
|
||||
case 'email':
|
||||
return 'email';
|
||||
case 'checkbox':
|
||||
return 'checkbox';
|
||||
case 'switch':
|
||||
return 'radio';
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
|
||||
get isNumeric(): boolean {
|
||||
return this.inputMode === 'numeric';
|
||||
return ['number', 'decimal'].includes(this.inputMode);
|
||||
}
|
||||
|
||||
get preparedMaxLength(): number | null {
|
||||
@@ -153,7 +147,6 @@ export class InputComponent {
|
||||
switch (this.type) {
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
case 'price':
|
||||
case 'number':
|
||||
inputClass.push('ltrInput');
|
||||
break;
|
||||
@@ -174,14 +167,12 @@ export class InputComponent {
|
||||
switch (this.type) {
|
||||
case 'email':
|
||||
return 'email';
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
case 'postalCode':
|
||||
case 'nationalId':
|
||||
return 'number';
|
||||
case 'price':
|
||||
// case 'mobile':
|
||||
// case 'phone':
|
||||
// case 'postalCode':
|
||||
// case 'nationalId':
|
||||
case 'number':
|
||||
return 'text';
|
||||
return 'number';
|
||||
case 'checkbox':
|
||||
return 'checkbox';
|
||||
case 'switch':
|
||||
@@ -195,10 +186,6 @@ export class InputComponent {
|
||||
return this.required || this.control.hasValidator(Validators.required);
|
||||
}
|
||||
|
||||
onPriceInput($event: InputNumberInputEvent) {
|
||||
this.onInput($event.originalEvent, true);
|
||||
}
|
||||
|
||||
private toEnglishDigits(value: string): string {
|
||||
return value
|
||||
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776))
|
||||
@@ -207,6 +194,7 @@ export class InputComponent {
|
||||
|
||||
private sanitizeNumericValue(value: string, allowDecimal: boolean): string {
|
||||
const normalized = this.toEnglishDigits(value).replace(/,/g, '');
|
||||
|
||||
const cleaned = normalized.replace(allowDecimal ? /[^0-9.]/g : /[^0-9]/g, '');
|
||||
if (!allowDecimal) return cleaned;
|
||||
|
||||
@@ -245,76 +233,82 @@ export class InputComponent {
|
||||
return value.replace(/^0+(?=\d)/, '');
|
||||
}
|
||||
|
||||
onInput($event: Event, isPriceFormat?: boolean) {
|
||||
if (this.type !== 'price' && this.type !== 'number') {
|
||||
return;
|
||||
private enforceMaxLength(value: string): string {
|
||||
if (!this.preparedMaxLength || value.length <= this.preparedMaxLength) return value;
|
||||
return value.slice(0, this.preparedMaxLength);
|
||||
}
|
||||
|
||||
private parseNumericValue(value: string): number {
|
||||
return Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
||||
}
|
||||
|
||||
private isOutOfRange(numericValue: number): { min: boolean; max: boolean } {
|
||||
return {
|
||||
min: !!((this.min || this.min === 0) && numericValue < this.min),
|
||||
max: !!((this.max || this.max === 0) && numericValue > this.max),
|
||||
};
|
||||
}
|
||||
|
||||
private notifyRangeError(range: { min: boolean; max: boolean }) {
|
||||
if (range.max) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max} باشد.`,
|
||||
});
|
||||
}
|
||||
if (range.min) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isIdentifierField(): boolean {
|
||||
return ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
|
||||
}
|
||||
|
||||
private writeControlAndViewValue(target: HTMLInputElement, value: string, numericValue: number) {
|
||||
const isIdentifierField = this.isIdentifierField();
|
||||
const controlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
|
||||
this.control.setValue(controlValue);
|
||||
target.value = value;
|
||||
}
|
||||
|
||||
onInput($event: Event) {
|
||||
const target = $event.target as HTMLInputElement | null;
|
||||
if (!target) return;
|
||||
|
||||
let value = target.value ?? '';
|
||||
const allowDecimal = this.type === 'price' || this.type === 'number';
|
||||
const allowDecimal = this.type === 'number';
|
||||
const isNumericMode = this.isNumeric;
|
||||
|
||||
if (this.inputMode === 'numeric' || this.inputMode === 'decimal' || isPriceFormat) {
|
||||
if (isNumericMode) {
|
||||
value = this.sanitizeNumericValue(value, allowDecimal);
|
||||
value = this.applyFixed(value);
|
||||
value = this.normalizeLeadingZeros(value, allowDecimal);
|
||||
}
|
||||
|
||||
if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
|
||||
value = value.slice(0, this.preparedMaxLength);
|
||||
}
|
||||
value = this.enforceMaxLength(value);
|
||||
|
||||
let numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
||||
const minValidator = (this.min || this.min === 0) && numericValue < this.min!;
|
||||
const maxValidator = (this.max || this.max === 0) && numericValue > this.max!;
|
||||
if (
|
||||
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
|
||||
value !== '' &&
|
||||
(minValidator || maxValidator)
|
||||
) {
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max} باشد.`,
|
||||
});
|
||||
let numericValue = this.parseNumericValue(value);
|
||||
const range = this.isOutOfRange(numericValue);
|
||||
if (value !== '' && (range.min || range.max)) {
|
||||
this.notifyRangeError(range);
|
||||
value = this.lastValidNumericValue;
|
||||
numericValue = this.parseNumericValue(value);
|
||||
this.writeControlAndViewValue(target, value, numericValue);
|
||||
return;
|
||||
} else {
|
||||
this.lastValidNumericValue = value;
|
||||
}
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
||||
});
|
||||
|
||||
this.writeControlAndViewValue(target, value, numericValue);
|
||||
if (this.valueChange.observed) {
|
||||
this.valueChange.emit(this.isIdentifierField() ? value : numericValue);
|
||||
}
|
||||
value = this.lastValidNumericValue;
|
||||
numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
||||
|
||||
// const isIdentifierField = ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
|
||||
const isIdentifierField = !this.numericValue;
|
||||
const restoredControlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
|
||||
|
||||
this.control.setValue(restoredControlValue);
|
||||
target.value = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
|
||||
return;
|
||||
} else if (this.inputMode === 'numeric' || this.inputMode === 'decimal') {
|
||||
this.lastValidNumericValue = value;
|
||||
}
|
||||
|
||||
const isIdentifierField = ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
|
||||
const controlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
|
||||
|
||||
this.control.setValue(controlValue);
|
||||
|
||||
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
|
||||
|
||||
target.value = viewValue;
|
||||
if (
|
||||
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
|
||||
this.valueChange.observed
|
||||
) {
|
||||
this.valueChange.emit(isIdentifierField ? value : numericValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const allowDecimal = this.type === 'price' || this.type === 'number';
|
||||
const allowDecimal = this.type === 'number';
|
||||
const initial = this.sanitizeNumericValue(`${this.control?.value ?? ''}`, allowDecimal);
|
||||
this.lastValidNumericValue = initial;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<uikit-field [label]="label" [name]="name" [control]="control" [showLabel]="!!label" [showErrors]="showErrors">
|
||||
<p-inputgroup>
|
||||
<p-inputnumber
|
||||
[attr.id]="name"
|
||||
[formControl]="control"
|
||||
[attr.name]="name"
|
||||
[attr.placeholder]="preparedPlaceholder"
|
||||
[attr.type]="'text'"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[attr.disabled]="disabled"
|
||||
[class]="`ltrInput rtlPlaceholder hasSuffix w-full ${sizeClass}`"
|
||||
inputStyleClass="hasSuffix"
|
||||
[required]="isRequired"
|
||||
[invalid]="control.invalid && (control.touched || control.dirty)"
|
||||
(onInput)="onInput($event)"
|
||||
(onBlur)="blur.emit()" />
|
||||
@if (suffixTemp) {
|
||||
<ng-container [ngTemplateOutlet]="suffixTemp" />
|
||||
} @else {
|
||||
<p-inputgroup-addon [ngClass]="{ 'border-red-400!': (control.touched || control.dirty) && control.invalid }">
|
||||
ریال
|
||||
</p-inputgroup-addon>
|
||||
}
|
||||
</p-inputgroup>
|
||||
@if (hint) {
|
||||
<small [attr.id]="name + '-help'" class="text-muted-color">{{ hint }}</small>
|
||||
}
|
||||
</uikit-field>
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ContentChild,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { InputGroupModule } from 'primeng/inputgroup';
|
||||
import { InputGroupAddon } from 'primeng/inputgroupaddon';
|
||||
import { InputNumberInputEvent, InputNumberModule } from 'primeng/inputnumber';
|
||||
|
||||
@Component({
|
||||
selector: 'app-price-input',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
InputGroupModule,
|
||||
InputGroupAddon,
|
||||
InputNumberModule,
|
||||
UikitFieldComponent,
|
||||
CommonModule,
|
||||
],
|
||||
templateUrl: './price-input.component.html',
|
||||
})
|
||||
export class PriceInputComponent {
|
||||
@Input() control!: FormControl<Maybe<any>>;
|
||||
@Input() name!: string;
|
||||
@Input() label = '';
|
||||
@Input() placeholder?: string;
|
||||
@Input() disabled = false;
|
||||
@Input() size?: 'small' | 'large';
|
||||
@Input() autocomplete: string = 'off';
|
||||
@Input() showErrors = true;
|
||||
@Input() hint?: string;
|
||||
@Input() min?: number = 0;
|
||||
@Input() max?: number;
|
||||
@Input() required = false;
|
||||
@Output() inputEvent = new EventEmitter<InputNumberInputEvent>();
|
||||
@Output() blur = new EventEmitter<void>();
|
||||
|
||||
@ContentChild('suffixTemp', { static: true }) suffixTemp!: TemplateRef<any> | null;
|
||||
|
||||
private readonly toastService = inject(ToastService);
|
||||
|
||||
get preparedPlaceholder(): string {
|
||||
return this.placeholder || 'مبلغ';
|
||||
}
|
||||
|
||||
get isRequired(): boolean {
|
||||
return this.required || this.control.hasValidator(Validators.required);
|
||||
}
|
||||
|
||||
get sizeClass(): string {
|
||||
return this.size === 'large' ? 'p-inputtext-lg' : this.size === 'small' ? 'p-inputtext-sm' : '';
|
||||
}
|
||||
|
||||
onInput(event: InputNumberInputEvent) {
|
||||
console.log('event', event);
|
||||
|
||||
const eventValue = Number(event.value ?? 0);
|
||||
const min = this.min ?? undefined;
|
||||
const max = this.max ?? undefined;
|
||||
|
||||
const minValidator = min !== undefined && eventValue < min;
|
||||
const maxValidator = max !== undefined && eventValue > max;
|
||||
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max!.toLocaleString('en-US')} باشد.`,
|
||||
});
|
||||
}
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min!.toLocaleString('en-US')} باشد.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (minValidator || maxValidator) {
|
||||
const normalizedValue = parseFloat(
|
||||
eventValue.toString().slice(0, eventValue.toString().length - 1) || '0'
|
||||
);
|
||||
|
||||
this.control.setValue(normalizedValue);
|
||||
// @ts-ignore
|
||||
event.originalEvent.target.value = normalizedValue
|
||||
? normalizedValue.toLocaleString('en-US')
|
||||
: normalizedValue;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
event.originalEvent.target.value = eventValue
|
||||
? eventValue.toLocaleString('en-US')
|
||||
: eventValue;
|
||||
}
|
||||
|
||||
this.inputEvent.emit({
|
||||
...event,
|
||||
value: eventValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user