int-field.component.ts (1-317)


import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  OnInit,
  inject,
  input,
  output,
  signal,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { startWith } from 'rxjs';
import { fieldGeneratorMap, GeneratorType } from '../schema-config.model';
import { compareToOtherFieldValidator } from '../../shared/validators/compare-to-other-field-validator.validator';
import { matchingArrayLengthsValidator } from '../../shared/validators/matching-array-lenghts-validator.validator';
import { SchemaFormFieldGeneratorService } from '../../shared/services/schema-form-field-generator.service';
import { TitleCasePipe } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { RepeatMaskDirective } from '../../shared/directives/repeat-mask.directive';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { ErrorMessagePipe } from '../../shared/pipes/error-message.pipe';
import { integersCommaSeparatedValidator } from '../../shared/validators/integers-comma-separate-validator.validator';
import { isIntegerValidator } from '../../shared/validators/is-integer-validator.validator';

type IntFieldGenerator =
  | { type: GeneratorType.Uniform; min: number; max: number }
  | { type: GeneratorType.Enum; values: number[]; weights: number[] }
  | { type: GeneratorType.NumericRange; start: number; step: number }
  | { type: GeneratorType.Values; values: number[] };

interface IntFieldForm {
  type: FormControl<GeneratorType>;
  min?: FormControl<number>;
  max?: FormControl<number>;
  values?: FormControl<string>;
  weights?: FormControl<string>;
  start?: FormControl<number>;
  step?: FormControl<number>;
}

@Component({
  selector: 'app-int-field',
  standalone: true,
  imports: [
    MatFormFieldModule,
    MatSelectModule,
    ReactiveFormsModule,
    MatInputModule,
    ReactiveFormsModule,
    TitleCasePipe,
    RepeatMaskDirective,
    MatButtonModule,
    MatIconModule,
    ErrorMessagePipe,
  ],
  templateUrl: './int-field.component.html',
  styleUrl: './int-field.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IntFieldComponent implements OnInit {
  private readonly schemaFormFieldGeneratorSvc = inject(
    SchemaFormFieldGeneratorService,
  );
  private readonly destroyRef = inject(DestroyRef);

  public readonly autoRenew = signal(true);

  public readonly generatorTypes =
    input.required<typeof fieldGeneratorMap.int_col>();

  public readonly changeField = output<{
    schema: IntFieldGenerator | undefined;
    valid: boolean;
  }>();

  public readonly type = this.schemaFormFieldGeneratorSvc.getTypeField();
  public readonly min = this.schemaFormFieldGeneratorSvc.getMinField([
    Validators.min(-2147483648),
    isIntegerValidator,
    compareToOtherFieldValidator(
      'max',
      (min: number, max: number) => +min < +max,
      'minGreaterThanMax',
    ),
  ]);
  public readonly max = this.schemaFormFieldGeneratorSvc.getMaxField([
    Validators.max(2147483648),
    isIntegerValidator,
    compareToOtherFieldValidator(
      'min',
      (max: number, min: number) => +min < +max,
      'MaxSmallerThanMin',
    ),
  ]);
  public readonly values = this.schemaFormFieldGeneratorSvc.getValuesField([
    integersCommaSeparatedValidator,
  ]);
  public readonly weights = this.schemaFormFieldGeneratorSvc.getWeightsField();
  public readonly start = this.schemaFormFieldGeneratorSvc.getStartField();
  public readonly step = this.schemaFormFieldGeneratorSvc.getStepField();
  public readonly fieldForm = new FormGroup<IntFieldForm>({
    type: this.type,
  });

  ngOnInit(): void {
    this.type.valueChanges
      .pipe(startWith(this.type.value), takeUntilDestroyed(this.destroyRef))
      .subscribe((type) => {
        this.updateFormControls(type);
      });

    this.fieldForm.valueChanges
      .pipe(
        startWith(this.fieldForm.value),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe(() => {
        const schema = this.getTypedFormValue();
        this.changeField.emit({
          schema,
          valid: this.fieldForm.valid,
        });
      });

    this.min.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        this.max.updateValueAndValidity({ emitEvent: false });
      });

    this.max.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        this.min.updateValueAndValidity({ emitEvent: false });
      });

    this.values.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        if (this.autoRenew() && this.values.valid) this.updateWeights();
      });

    this.weights.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        this.values.updateValueAndValidity({ emitEvent: false });
      });
  }

  private updateFormControls(type: GeneratorType): void {
    switch (type) {
      case GeneratorType.Uniform:
        this.fieldForm.removeControl('values');
        this.fieldForm.removeControl('weights');
        this.fieldForm.removeControl('start');
        this.fieldForm.removeControl('step');

        this.fieldForm.addControl('min', this.min);
        this.fieldForm.addControl('max', this.max);

        break;

      case GeneratorType.Enum:
        this.fieldForm.removeControl('min');
        this.fieldForm.removeControl('max');
        this.fieldForm.removeControl('start');
        this.fieldForm.removeControl('step');

        this.fieldForm.addControl('values', this.values);
        this.fieldForm.addControl('weights', this.weights);

        this.updateValuesAndWeightsValidation();

        break;

      case GeneratorType.NumericRange:
        this.fieldForm.removeControl('min');
        this.fieldForm.removeControl('max');
        this.fieldForm.removeControl('values');
        this.fieldForm.removeControl('weights');

        this.fieldForm.addControl('start', this.start);
        this.fieldForm.addControl('step', this.step);
        break;

      case GeneratorType.Values:
        this.fieldForm.removeControl('min');
        this.fieldForm.removeControl('max');
        this.fieldForm.removeControl('weights');
        this.fieldForm.removeControl('start');
        this.fieldForm.removeControl('step');

        this.fieldForm.addControl('values', this.values);

        this.values.removeValidators(
          matchingArrayLengthsValidator(['weights']),
        );
        break;
    }
  }
  private getTypedFormValue(): IntFieldGenerator | undefined {
    const { type, min, max, values, weights, start, step } =
      this.fieldForm.value;

    switch (type) {
      case GeneratorType.Uniform:
        if (min && max) return { type, min: +min, max: +max };

        break;

      case GeneratorType.Enum:
        if (values && weights)
          return {
            type,
            values: values.split(',').map((v) => +v.trim()),
            weights: weights.split(',').map((v) => parseFloat(v)),
          };

        break;

      case GeneratorType.NumericRange:
        if (start && step)
          return {
            type,
            start: +start,
            step: +step,
          };

        break;

      case GeneratorType.Values:
        if (values)
          return {
            type,
            values: values.split(',').map((v) => +v.trim()),
          };

        break;
    }

    return undefined;
  }

  private calculateWeights(values: string): string {
    const valueArray = values
      .split(',')
      .map((v) => v.trim())
      .filter((v) => v !== '');

    if (valueArray.length === 0) return '';

    const weight = (1 / valueArray.length).toFixed(2);

    return Array(valueArray.length).fill(weight).join(', ');
  }

  private updateValuesAndWeightsValidation(): void {
    if (this.autoRenew()) {
      this.values.removeValidators(matchingArrayLengthsValidator(['weights']));
      this.weights.removeValidators(matchingArrayLengthsValidator(['values']));
    } else {
      this.values.addValidators(matchingArrayLengthsValidator(['weights']));
      this.weights.addValidators(matchingArrayLengthsValidator(['values']));
    }

    this.values.updateValueAndValidity({ emitEvent: false });
    this.weights.updateValueAndValidity({ emitEvent: false });
  }

  private updateWeights(): void {
    const calculatedWeights = this.calculateWeights(this.values.value || '');
    this.weights.setValue(calculatedWeights, { emitEvent: true });
  }

  public onToggleAutorenew(): void {
    this.autoRenew.update((value) => !value);

    this.updateValuesAndWeightsValidation();

    if (this.autoRenew()) this.updateWeights();
  }

  public getErrorMessage(controlName: string): string {
    const control = this.fieldForm.get(controlName);

    if (control && control.invalid && (control.touched || control.dirty)) {
      const errors = control.errors;
      if (errors) {
        const errorMessages: { [key: string]: string } = {
          required: 'This field is required',
          min: `Minimum value is ${errors['min'].min}`,
          max: `Maximum value is ${errors['max'].max}`,
          minGreaterThanMax: 'Minimum value should be less than maximum value',
          MaxSmallerThanMin:
            'Maximum value should be greater than minimum value',
          numbersCommaSeparated: 'Please enter valid comma-separated numbers',
          matchingArrayLengths: 'The number of values and weights should match',
        };

        const firstErrorKey = Object.keys(errors)[0];
        return errorMessages[firstErrorKey] || 'Invalid input';
      }
    }

    return '';
  }
}


Can you spot the problem here regarding the auto renew? First time i'm using that it works fine and it's calculating ok the weights, The second i put on manual mode and then again on auto it seems the values print me the error "The lengths of the arrays do not match" but on auto that shouldn't be a validator because it's auto calculated