Используйте расширенную пользовательскую проверку в несколько шагов

Мой довольно сложный Stackblitz

Обычно, когда у меня сложная проверка в реактивных формах, я определяю formGroup для тех элементов управления, которые зависят друг от друга.

Это невозможно в вышеупомянутом сенарио, где мы имеем 3 steps = 3 groups и в зависимости от 3 поля firstUnique, secondUnique, thirdUnique,

<form [formGroup]="myForm">
<mat-horizontal-stepper formArrayName="formArray" #stepper>
  <mat-step formGroupName="0" [stepControl]="formArray?.get([0])" errorMessage="Name is required.">
      <ng-template matStepLabel>Fill out your name</ng-template>
      <mat-form-field>
        <input matInput placeholder="Last name, First name" formControlName="firstCtrl" required>
      </mat-form-field>
      <mat-form-field>
        <input matInput placeholder="UNIQUE1" formControlName="firstUnique" required>
      </mat-form-field>
      <div>
        <button mat-button matStepperNext>Next</button>
      </div>
  </mat-step>
  <mat-step formGroupName="1" [stepControl]="formArray?.get([1])" errorMessage="Address is required.">
      <ng-template matStepLabel>Fill out your address</ng-template>
      <mat-form-field>
        <input matInput placeholder="Address" formControlName="secondCtrl" required>
      </mat-form-field>
            <mat-form-field>
        <input matInput placeholder="UNIQUE2" formControlName="secondUnique" required>
      </mat-form-field>
      <div>
        <button mat-button matStepperPrevious>Back</button>
        <button mat-button matStepperNext>Next</button>
      </div>
  </mat-step>
  <mat-step formGroupName="2" [stepControl]="formArray?.get([2])" errorMessage="Error!"> 
    <ng-template matStepLabel>Done</ng-template>
    You are now done.
    <div>
      <mat-form-field>
        <input matInput placeholder="UNIQUE3" formControlName="thirdUnique" required>
      </mat-form-field>
      <button mat-button matStepperPrevious>Back</button>
      <button mat-button (click)="stepper.reset()">Reset</button>
    </div>
  </mat-step>

</mat-horizontal-stepper>

Я использую методы, описанные SO_answer и Material_docs

Мое Решение работает, но я не удовлетворен этим:

  1. На старте Unique Validation выполняется тысячу раз (30-40 раз) (хакер)
  2. На КАЖДОЕ изменение ЛЮБОГО ввода во всем степпере Unique Validation это триггер. (это потому, что мне пришлось добавить его во всю форму группы).
  3. Простая задача, поскольку эти три поля ввода должны быть УНИКАЛЬНЫМИ, превратилась в шаблон и сложный беспорядок. (пожалуйста, соблюдайте function Unique(arr: string[]))

  4. Когда правильный шаг становится недействительным UNIQUE Validator или STEP снова становится действительным, STEPPER-VALIDATION не вызывается. (пример: firstUnique = "a", secondUnique "b", thirdUnique = "a" (снова))

MyForm

this.myForm = this._formBuilder.group({
  formArray:
  this._formBuilder.array([
    this._formBuilder.group({
        firstCtrl: [''],
        firstUnique: [''],
    }),
    this._formBuilder.group({
        secondCtrl: [''],
        secondUnique: [''],
    }),
     this._formBuilder.group({
        thirdUnique: [''],
    })
  ])

}, {
  validator: [Unique(['0;firstUnique', '1;secondUnique', '2;thirdUnique'])]
});

Уникальный валидатор веселья

function Unique(arr: string[]) {

const validKey = "uniqueValid";

return (formGroup: FormGroup) => {

    const myValues =
    arr.map(path => {
      const s = path.split(';');
      return (<FormArray>formGroup.get('formArray'))
      .controls[parseInt(s[0])]
      .controls[s[1]];
    });

    const myKeys = arr.map(path => path.split(';')[1] )

    const obj = {};

    myKeys.forEach(function (k, i) {
      obj[k] = myValues[i];
    })

    myKeys.forEach((item, index) => {
      debugger
      console.log('unique validation function runs')

      const control = obj[item];

      const tmp = myKeys.slice();
      tmp.splice(index,1);

      const ans = tmp
      .filter( el => obj[item].value === obj[el].value)

      if ( ans.length && control.value ) {
        const err = {}
        err[validKey] = `identicial to: ${ans.join(', ')}`
        control.setErrors(err);
      } else if ( obj[item].errors && !obj[item].errors[validKey] ) {
        return; 
      } else {
        control.setErrors(null);
      }

    })
}

1 ответ

Используя библиотеку ngx-sub-form, вот живая демонстрация Stackblitz:

https://stackblitz.com/edit/ngx-sub-form-stepper-form-demo

Чтобы объяснить немного, это будет выглядеть следующим образом:

Во-первых, нам нужно определить некоторые интерфейсы, чтобы наш код был надежным и безопасным

шаговые-form.interface.ts

export interface Part1 {
  firstCtrl: string;
  firstUnique: string;
}

export interface Part2 {
  secondCtrl: string;
  secondUnique: string;
}

export interface Part3 {
  thirdUnique: string;
}

export interface StepperForm {
  part1: Part1;
  part2: Part2;
  part3: Part3;
}

От компонента верхнего уровня мы даже не хотим знать, что есть форма. Мы просто хотим, чтобы нас предупреждали, когда сохранено новое значение.

app.component.ts

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  public stepperFormUpdated(stepperForm: StepperForm): void {
    console.log(stepperForm);
  }
}

app.component.html

<app-stepper-form (stepperFormUpdated)="stepperFormUpdated($event)"></app-stepper-form>

Теперь мы начинаем использовать библиотеку, создаем форму верхнего уровня (root) и выставляем результат в качестве вывода. Мы также определяем ограничение, что 3 уникальных входа не должны иметь одинаковые значения.

шаговые-form.component.ts

@Component({
  selector: 'app-stepper-form',
  templateUrl: './stepper-form.component.html',
  styleUrls: ['./stepper-form.component.css']
})
export class StepperFormComponent extends NgxRootFormComponent<StepperForm> {
  @DataInput()
  @Input('stepperForm')
  public dataInput: StepperForm | null | undefined;

  @Output('stepperFormUpdated')
  public dataOutput: EventEmitter<StepperForm> = new EventEmitter();

  public send() {
    this.manualSave();
  }

  protected getFormControls(): Controls<StepperForm> {
    return {
      part1: new FormControl(),
      part2: new FormControl(),
      part3: new FormControl(),
    }
  }

  public getFormGroupControlOptions(): FormGroupOptions<StepperForm> {
    return {
      validators: [
        formGroup => {
          if (!formGroup || !formGroup.value || !formGroup.value.part1 || !formGroup.value.part2 || !formGroup.value.part3) {
            return null;
          }

          const values: string[] = [
            formGroup.value.part1.firstUnique,
            formGroup.value.part2.secondUnique,
            formGroup.value.part3.thirdUnique,
          ].reduce((acc, curr) => !!curr ? [...acc, curr] : acc, []);

          const valuesSet: Set<string> = new Set(values);

          if (values.length !== valuesSet.size) {
            return {
              sameValues: true
            };
          }

          return null;
        },
      ],
    };
  }
}

Время создавать наш шаблон с помощью утилит, предоставляемых библиотекой lib

шагового form.component.html

<form [formGroup]="formGroup">
  <mat-horizontal-stepper>
    <mat-step>
      <ng-template matStepLabel>First control</ng-template>

      <app-first-part [formControlName]="formControlNames.part1"></app-first-part>

      <button mat-button matStepperNext>Next</button>
    </mat-step>

    <mat-step>
      <ng-template matStepLabel>Second control</ng-template>

      <app-second-part [formControlName]="formControlNames.part2"></app-second-part>

      <button mat-button matStepperNext>Next</button>
    </mat-step>

    <mat-step>
      <ng-template matStepLabel>Third control</ng-template>

      <app-third-part [formControlName]="formControlNames.part3"></app-third-part>

      <button mat-button (click)="send()">Send the form</button>
    </mat-step>
  </mat-horizontal-stepper>
</form>

<div *ngIf="formGroupErrors?.formGroup?.sameValues">
  Same values, please provide different ones
</div>

Теперь давайте создадим наш первый субкомпонент

первоклассников part.component.ts

@Component({
  selector: 'app-first-part',
  templateUrl: './first-part.component.html',
  styleUrls: ['./first-part.component.css'],
  providers: subformComponentProviders(FirstPartComponent)
})
export class FirstPartComponent extends NgxSubFormComponent<Part1> {
  protected getFormControls(): Controls<Part1> {
    return {
      firstCtrl: new FormControl(),
      firstUnique: new FormControl(),
    }
  }
}

и его шаблон

первый-part.component.html

<div [formGroup]="formGroup">
  <mat-form-field>
    <input matInput placeholder="First" type="text" [formControlName]="formControlNames.firstCtrl">
  </mat-form-field>

  <mat-form-field>
    <input matInput type="text" placeholder="First unique" [formControlName]="formControlNames.firstUnique">
  </mat-form-field>
</div>

Тогда почти то же самое для second-part.component.html а также third-part.component.html так что я пропускаю это здесь.

Я предположил, что вам действительно не нужно FormArray в этом случае я не был уверен во всем коде проверки, который у вас был, поэтому я просто создал код, который выдает ошибки, если хотя бы 2 уникальных значения совпадают.

https://stackblitz.com/edit/ngx-sub-form-stepper-form-demo

Редактировать:

Если вы хотите пойти дальше, я только что опубликовал пост в блоге, чтобы объяснить много вещей о формах и ngx-sub-form здесь https://dev.to/maxime1992/building-scalable-robust-and-type-safe-forms-with-angular-3nf9

Другие вопросы по тегам