double-field.component.ts (1-336)


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 { fieldGeneratorMap, GeneratorType } from '../schema-config.model';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { SchemaFormFieldGeneratorService } from '../../shared/services/schema-form-field-generator.service';
import { compareToOtherFieldValidator } from '../../shared/validators/compare-to-other-field-validator.validator';
import { numbersCommaSeparatedValidator } from '../../shared/validators/numbers-comma-separate-validator.validator';
import { startWith } from 'rxjs';
import { matchingArrayLengthsValidator } from '../../shared/validators/matching-array-lenghts-validator.validator';
import { TitleCasePipe } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { RepeatMaskDirective } from '../../shared/directives/repeat-mask.directive';
import { ErrorMessagePipe } from '../../shared/pipes/error-message.pipe';

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

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

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

  public readonly autoRenew = signal(true);

  public generatorTypes =
    input.required<typeof fieldGeneratorMap.decimal_col>();
  public changeField = output<{
    schema: DoubleFieldGenerator | undefined;
    valid: boolean;
  }>();

  private matchingValuesWeightsValidator = matchingArrayLengthsValidator([
    'weights',
  ]);
  private matchingWeightsValuesValidator = matchingArrayLengthsValidator([
    'values',
  ]);

  public type = this.schemaFormFieldGeneratorSvc.getTypeField();
  public min = this.schemaFormFieldGeneratorSvc.getMinField({
    value: 1.0,
    validators: [
      Validators.min(Number.MIN_SAFE_INTEGER),
      compareToOtherFieldValidator(
        'max',
        (min: number, max: number) => +min < +max,
        'minGreaterThanMax',
      ),
    ],
  });
  public max = this.schemaFormFieldGeneratorSvc.getMaxField({
    value: 100.0,
    validators: [
      Validators.max(Number.MAX_SAFE_INTEGER),
      compareToOtherFieldValidator(
        'min',
        (max: number, min: number) => +min < +max,
        'MaxSmallerThanMin',
      ),
    ],
  });
  public mean = this.schemaFormFieldGeneratorSvc.getMeanField({
    value: 0.0,
  });
  public stdDev = this.schemaFormFieldGeneratorSvc.getStdDevField({
    value: 1.0,
  });
  public values = this.schemaFormFieldGeneratorSvc.getValuesField({
    validators: [numbersCommaSeparatedValidator],
  });
  public weights = this.schemaFormFieldGeneratorSvc.getWeightsField();
  public start = this.schemaFormFieldGeneratorSvc.getStartField({
    value: 1.0,
  });
  public step = this.schemaFormFieldGeneratorSvc.getStepField({
    value: 1.0,
  });
  public fieldForm = new FormGroup<DoubleFieldForm>({
    type: this.type,
  });

  public 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();
        if (schema)
          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.removeControl('mean');
        this.fieldForm.removeControl('stdDev');

        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.removeControl('mean');
        this.fieldForm.removeControl('stdDev');

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

        this.updateValuesAndWeightsValidation();
        break;

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

        this.fieldForm.addControl('mean', this.mean);
        this.fieldForm.addControl('stdDev', this.stdDev);
        break;

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

        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.removeControl('mean');
        this.fieldForm.removeControl('stdDev');

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

        this.values.removeValidators(this.matchingValuesWeightsValidator);
        break;
    }
  }
  private getTypedFormValue(): DoubleFieldGenerator | undefined {
    const { type, min, max, mean, stdDev, 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.Normal:
        if (mean && stdDev) return { type, mean: +mean, std_dev: +stdDev };

        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(this.matchingValuesWeightsValidator);
      this.weights.removeValidators(this.matchingWeightsValuesValidator);
    } else {
      this.values.addValidators(this.matchingValuesWeightsValidator);
      this.weights.addValidators(this.matchingWeightsValuesValidator);
    }

    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();
  }
}


double-field.component.html (1-125)


<form class="row" [formGroup]="fieldForm">
  <div class="col-12">
    <mat-form-field class="w-100">
      <mat-label>Generator</mat-label>
      <mat-select [formControl]="type">
        @for (generator of generatorTypes(); track generator) {
          <mat-option [value]="generator.value">{{
            generator.viewValue
          }}</mat-option>
        }
      </mat-select>
      <mat-hint>Choose how to generate values for this field</mat-hint>
    </mat-form-field>
  </div>

  @if (fieldForm.get("min")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Minimum Value</mat-label>
        <input matInput formControlName="min" placeholder="e.g., 1.0" />
        <mat-hint>Enter the minimum value of the range</mat-hint>
        <mat-error>{{ fieldForm.get("min") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("max")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Maximum Value</mat-label>
        <input matInput formControlName="max" placeholder="e.g., 100.0" />
        <mat-hint>Enter the maximum value of the range</mat-hint>
        <mat-error>{{ fieldForm.get("max") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("mean")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Mean</mat-label>
        <input matInput formControlName="mean" placeholder="e.g., 50.0" />
        <mat-hint
          >Enter the average value (mean) for the normal distribution</mat-hint
        >
        <mat-error>{{ fieldForm.get("mean") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("stdDev")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Standard Deviation</mat-label>
        <input matInput formControlName="stdDev" placeholder="e.g., 5.0" />
        <mat-hint
          >Enter the standard deviation for the normal distribution</mat-hint
        >
        <mat-error>{{ fieldForm.get("stdDev") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("values")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Values</mat-label>
        <input
          placeholder="e.g., 10.0, 20.0, 30.0"
          matInput
          formControlName="values"
        />
        <mat-hint>Enter numeric values separated by commas</mat-hint>
        <mat-error>{{ fieldForm.get("values") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("weights")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Weights</mat-label>
        <input
          [readonly]="autoRenew()"
          placeholder="e.g., 0.3, 0.3, 0.4"
          matInput
          formControlName="weights"
        />
        <mat-hint>
          Enter decimal weights between 0.0 and 1.0, separated by commas. The
          weights should sum to 1.0.
        </mat-hint>
        <mat-error>{{ fieldForm.get("weights") | appErrorMessage }}</mat-error>
        <button (click)="onToggleAutorenew()" matIconSuffix mat-icon-button>
          <mat-icon>{{ autoRenew() ? "autorenew" : "pause" }}</mat-icon>
        </button>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("start")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Start</mat-label>
        <input placeholder="e.g., 0.0" matInput formControlName="start" />
        <mat-hint>Enter the starting value of the sequence</mat-hint>
        <mat-error>{{ fieldForm.get("start") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("step")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Step</mat-label>
        <input placeholder="e.g., 1.0" matInput formControlName="step" />
        <mat-hint
          >Enter the step size to increment or decrement the value</mat-hint
        >
        <mat-error>{{ fieldForm.get("step") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }
</form>


schema-form-field-generator.service.ts (1-339)


import { Injectable } from '@angular/core';
import {
  FormArray,
  FormControl,
  FormGroup,
  ValidatorFn,
  Validators,
} from '@angular/forms';
import {
  FieldTypes,
  GeneratorType,
} from '../../schema-config/schema-config.model';
import {
  SchemaConfigForm,
  SchemaFieldFormGroup,
} from '../interfaces/schema.interface';
import { weightsSumToOneValidator } from '../validators/weights-sum-to-one-validator.validator';
import { numbersCommaSeparatedValidator } from '../validators/numbers-comma-separate-validator.validator';

interface FormControlOptions<T> {
  value?: T;
  validators?: ValidatorFn[];
}

@Injectable({
  providedIn: 'root',
})
export class SchemaFormFieldGeneratorService {
  private createFormControl<T>(
    defaultValue: T,
    validators: ValidatorFn[] = [],
  ): FormControl<T> {
    return new FormControl<T>(defaultValue, {
      nonNullable: true,
      validators: [...validators],
    });
  }

  public getScaleField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [
      Validators.required,
      Validators.pattern(/^[1-9]\d*$/),
      ...(options.validators || []),
    ];

    return this.createFormControl<number>(options.value ?? 1, validators);
  }

  public getTypeField(
    options: FormControlOptions<GeneratorType> = {},
  ): FormControl<GeneratorType> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<GeneratorType>(
      options.value ?? GeneratorType.Uniform,
      validators,
    );
  }

  public getMinField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<number>(options.value ?? 1, validators);
  }

  public getMaxField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<number>(options.value ?? 100, validators);
  }

  public getMeanField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [
      Validators.required,
      Validators.min(0),
      ...(options.validators || []),
    ];

    return this.createFormControl<number>(options.value ?? 0, validators);
  }

  public getStdDevField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [
      Validators.required,
      Validators.min(0),
      ...(options.validators || []),
    ];

    return this.createFormControl<number>(options.value ?? 1, validators);
  }

  public getValuesField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<string>(options.value ?? '', validators);
  }

  public getWeightsField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [
      Validators.required,
      numbersCommaSeparatedValidator,
      weightsSumToOneValidator,
      ...(options.validators || []),
    ];

    return this.createFormControl<string>(options.value ?? '', validators);
  }

  public getStartField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<number>(options.value ?? 1, validators);
  }

  public getEndField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<number>(options.value ?? 1, validators);
  }

  public getStartTimeField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<string>(
      options.value ?? '000000',
      validators,
    );
  }

  public getEndTimeField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<string>(
      options.value ?? '235959',
      validators,
    );
  }

  public getStartDateField(
    options: FormControlOptions<Date> = {},
  ): FormControl<Date> {
    const validators = [Validators.required, ...(options.validators || [])];
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    return this.createFormControl<Date>(options.value ?? today, validators);
  }

  public getEndDateField(
    options: FormControlOptions<Date> = {},
  ): FormControl<Date> {
    const validators = [Validators.required, ...(options.validators || [])];
    const nextWeek = new Date();
    nextWeek.setHours(0, 0, 0, 0);
    nextWeek.setDate(nextWeek.getDate() + 7);

    return this.createFormControl<Date>(options.value ?? nextWeek, validators);
  }

  public getStartDateTimeField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];
    const now = new Date();
    const startDateTime = this.formatLocalDateTime(now);

    return this.createFormControl<string>(
      options.value ?? startDateTime,
      validators,
    );
  }

  public getEndDateTimeField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];
    const endDate = new Date();
    endDate.setDate(endDate.getDate() + 7);
    const endDateTime = this.formatLocalDateTime(endDate);

    return this.createFormControl<string>(
      options.value ?? endDateTime,
      validators,
    );
  }

  public getStartTimeStampField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];
    const now = new Date();
    const startTimestamp = now.toISOString();

    return this.createFormControl<string>(
      options.value ?? startTimestamp,
      validators,
    );
  }

  public getEndTimestampField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];
    const endDate = new Date();
    endDate.setDate(endDate.getDate() + 7);
    const endTimestamp = endDate.toISOString();

    return this.createFormControl<string>(
      options.value ?? endTimestamp,
      validators,
    );
  }

  public getStepField(
    options: FormControlOptions<number> = {},
  ): FormControl<number> {
    const validators = [
      Validators.required,
      Validators.pattern(/^-?(?!0(?:\.0+)?$)\d*\.?\d+$/),
      ...(options.validators || []),
    ];

    return this.createFormControl<number>(options.value ?? 1, validators);
  }

  public getDurationField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<string>(options.value ?? '1 day', validators);
  }

  public getDescriptionField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<string>(options.value ?? '', validators);
  }

  public getTimePatternField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<string>(
      options.value ?? 'HH:mm:ss',
      validators,
    );
  }

  public getDatePatternField(
    options: FormControlOptions<string> = {},
  ): FormControl<string> {
    const validators = [Validators.required, ...(options.validators || [])];

    return this.createFormControl<string>(
      options.value ?? 'yyyy-MM-dd',
      validators,
    );
  }

  public getSchemaConfigForm(
    isArray: boolean = false,
    isOptional: boolean = false,
  ): FormGroup<SchemaConfigForm> {
    return new FormGroup({
      generatorsArray: new FormArray([
        this.getGeneratorArray(isArray, isOptional),
      ]),
    });
  }

  public getGeneratorArray(
    isArray: boolean = false,
    isOptional: boolean = false,
  ): FormGroup<SchemaFieldFormGroup> {
    const group: Partial<SchemaFieldFormGroup> = {
      type: new FormControl<FieldTypes | null>(null, {
        validators: [Validators.required],
      }),
      schema: new FormControl<Record<string, any> | null>({}),
      arrayLength: new FormControl<number>(3, {
        nonNullable: true,
        validators: [Validators.required, Validators.min(0)],
      }),
      nullProbability: new FormControl<number>(0.5, {
        nonNullable: true,
        validators: [Validators.required, Validators.min(0), Validators.max(1)],
      }),
    };

    if (!isArray && !isOptional) {
      group.name = new FormControl('', {
        nonNullable: true,
        validators: [Validators.required],
      });
    }

    return new FormGroup(group as SchemaFieldFormGroup);
  }

  private formatLocalDateTime(date: Date): string {
    const pad = (n: number) => (n < 10 ? '0' + n : n.toString());

    const year = date.getFullYear();
    const month = pad(date.getMonth() + 1);
    const day = pad(date.getDate());
    const hours = pad(date.getHours());
    const minutes = pad(date.getMinutes());
    const seconds = pad(date.getSeconds());

    return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
  }
}


Do you have any idea why i'm seeing the integer number instead of the whole decimal in the html? When i enter on double field instead of 1.0 for Minimum value i see 1