Связывание двух полей в валидаторе Angular

У меня есть форма Angular 9, в которой связаны четыре поля. Один - это флажок, а остальные - ввод. Когда флажок установлен, поля ввода не должны быть пустыми, но когда он не установлен, это не имеет значения. Я хочу сделать для этого валидаторы, чтобы ошибки появлялись только тогда, когда поле пусто, а для первого поля установлено значение true.

Я также подумал о создании локального логического значения, представляющего состояние галочки, и передачи его валидатору вот так.

export function linkedFieldValidator(toggler: boolean): ValidatorFn {
  console.log('updated');
  return (control: AbstractControl): {[key: string]: any} | null => {
    return (toggler && control.value === '') ? {linkedField: {value: control.value}} : null;
  };
}

...
field: new FormControl('', linkedFieldValidator(this.checkboxvalue)),
...

Однако я полагаю, что это не работает, поскольку он передает значение логического значения только один раз и не обновляется после. Даже вызов updateValueAndValidity() не работает, что для меня странно (если это не так, то в чем его цель?).

Структура моего FormGroup выглядит примерно так:

this.form = this.formBuilder.group({
  name: new FormControl(''), // don't care
  address: new FormControl(''), // don't care
  car: new FormControl(false), // do care - this is the checkmark
  license_plate: new FormControl('', Validators.pattern(MY_LICENSE_PLATE_REGEX)), // shouldn't be empty when car
  mileage: new FormControl('') // shouldn't be empty when car
  hair: new FormControl(false), // do care - this is the checkmark
  hair_color: new FormControl(''), // shouldn't be empty when hair
});

Как видите, у меня есть пара FormControll друг через друга, и я хочу, чтобы была связана только пара из них. Еще одна важная вещь, которую следует отметить, заключается в том, что, хотя вся форма может стать недействительной при нарушении одного из этих условий, я хочу иметь возможность обрабатывать каждую ошибку индивидуально, чтобы я мог отображать правильные сообщения в нужных местах.

У меня больше нет идей, кто-нибудь может мне помочь? Использую реактивные формы.


person Luctia    schedule 14.05.2020    source источник
comment
Вы используете реактивные формы?   -  person Anton    schedule 14.05.2020
comment
@Anton Извините, что я забыл об этом упомянуть, да, это так.   -  person Luctia    schedule 14.05.2020


Ответы (2)


Проблема в том, что вы передаете только начальное значение функции linkFieldValidator.

Чтобы получить значение динамически, вы можете передать linkFieldValidator через FormGroup, например:

readonly formGroup = this.formBuilder.group(
  {
    checkbox: '',
    name: ''
  },
  { validator: linkedFieldValidator }
);

Полный образец:

import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
  AbstractControl,
  FormBuilder,
  FormGroup,
  ValidationErrors,
  ValidatorFn,
} from '@angular/forms';

export const linkedFieldValidator = (formGroup: FormGroup): ValidationErrors | null => {
  const [checkboxFormControlValue, nameFormControlValue] = [
    formGroup.get('checkbox')!.value,
    formGroup.get('name')!.value
  ];

  return checkboxFormControlValue && !nameFormControlValue
    ? { linkedField: { value: nameFormControlValue } }
    : null;
};

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'input-overview-example',
  styleUrls: ['input-overview-example.css'],
  templateUrl: 'input-overview-example.html'
})
export class InputOverviewExample {
  readonly formGroup = this.formBuilder.group(
    {
      checkbox: '',
      name: ''
    },
    { validator: linkedFieldValidator }
  );

  constructor(private readonly formBuilder: FormBuilder) {}
}

ДЕМО


Изменить 1: если вам нужно, чтобы ошибка постоянно присутствовала в каждом элементе управления формы, вы можете изменить свой linkedFieldValidator на:

export const linkedFieldValidator = (formGroup: FormGroup): null => {
  const { value: checkboxFormControlValue } = formGroup.get('checkbox')!;
  const inputFormControls = [
    formGroup.get('input1')!,
    formGroup.get('input2')!,
    formGroup.get('input3')!,
  ];

  inputFormControls.forEach(inputFormControl => {
    const { value } = inputFormControl;
    const errors = checkboxFormControlValue && !value ? { linkedField: { value } } : null;
    inputFormControl.setErrors(errors);
  });

  return null;
};

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

ДЕМО

Изменить 2:

Для общего подхода, когда у вас может быть несколько связанных полей, вы можете сделать что-то вроде этого:

type LinkedFormControl = Record<string, string | readonly string[]>;

const arrayify = <T>(itemOrItems: T | readonly T[]): readonly T[] => {
  return Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
};

const getErrorObjectSanitized = <T extends object>(obj: T): T | null => {
  return Object.keys(obj).length === 0 ? null : obj;
};

const getErrorsFor = (
  checkerValue: boolean,
  formControl: FormControl,
): object | null => {
  const { errors, value } = formControl;
  const { error, ...oldErrors } = errors || {};
  const processedErrors = {
    ...(checkerValue && !value ? { error: true } : {}),
    ...oldErrors,
  };

  return getErrorObjectSanitized(processedErrors);
};

export const linkedFieldValidator = (linkedFormControls: LinkedFormControl) => {
  return (formGroup: FormGroup): ValidationErrors | null => {
    Object.keys(linkedFormControls).forEach(key => {
      const { value: checkerValue } = formGroup.get(key)!;
      const dependentKeys = arrayify(linkedFormControls[key]);

      dependentKeys
        .map(dependentKey => formGroup.get(dependentKey)!)
        .forEach((dependentFormControl: FormControl) => {
          dependentFormControl.setErrors(
            getErrorsFor(checkerValue, dependentFormControl),
          );
        });
    });

    return null;
  };
};

... и звонок будет таким:

{
  validator: linkedFieldValidator({
    car: ['license_plate', 'mileage'],
    hair: 'hair_color',
  }),
},

ДЕМО

person developer033    schedule 14.05.2020
comment
Я видел такую ​​возможность, но, как я уже упоминал в своем сообщении, для меня это не работает, так как я хочу связать один флажок с тремя входами. Насколько я могу судить, это приводит к недействительности всего FormGroup, чего я не хочу; только пустые поля (когда флажок установлен) должны быть недействительными. - person Luctia; 14.05.2020
comment
Возможно, ваш пост совсем непонятен. Не могли бы вы создать стек, демонстрирующий проблему? - person developer033; 14.05.2020
comment
@Luctia Насколько я могу судить, это делает всю FormGroup недействительной, чего я не хочу; только пустые поля (когда флажок установлен) должны быть недействительными. Я забыл процитировать ваше предложение раньше, но стоит сказать кое-что об этом: если какие-либо из AbstractControl присутствует в FormGroup недействительно, FormGroup также недействителен. Таким образом, вы не можете пометить только конкретный AbstractControl как недопустимый без автоматической пометки FormGroup. - person developer033; 14.05.2020
comment
При этом я пытаюсь понять, чего вы пытаетесь достичь с помощью вашего вопроса, и я придумал это. Не могли бы вы взглянуть? - person developer033; 14.05.2020
comment
Думаю, этот пример у меня работает. В моей группе есть несколько таких случаев, но я думаю, что смогу с этим поработать. Еще одно замечание к вашему предыдущему комментарию, я думаю, что какое-то время я неправильно понимал, что это значит, и, борясь с этой проблемой, я начал понимать, что это значит, и вы правы, возможно, это было хорошо сформулировать это по-другому. Я собираюсь еще раз взглянуть на свой вопрос, если вы добавите то, что вы придумали, к своему ответу, я помечу его как ответ. Спасибо за вашу помощь! - person Luctia; 15.05.2020
comment
Я также обновил вопрос, если вы хотите взглянуть и сообщить мне, если то, что я хочу, невозможно с моей текущей структурой, это будет оценено: P Я думаю, что это должно быть возможно. - person Luctia; 15.05.2020
comment
@ Люкция, я посмотрю. Дай мне несколько минут :) - person developer033; 15.05.2020
comment
Еще один вопрос: похоже, это мешает существующим ошибкам, например Validators.pattern(). У меня есть декларации, как в моем вопросе, и скажем, я добавил валидатор шаблона к своему номерному знаку. После реализации вашего предложения действительность поля больше не зависит от этого. Могут ли валидатор для всей группы и валидатор для конкретного элемента управления не сосуществовать? - person Luctia; 15.05.2020
comment
Ах, это известная проблема, с которой я много раз сталкивался в своих приложениях. Я отредактировал ответ, объясняя это. Кстати, вы можете проверить старую проблему и старый PR, чтобы сделать его более удобным. - person developer033; 15.05.2020
comment
@Luctia Я снова обновил ответ :) Обратите внимание, что кто-то мог бы придумать это решение намного раньше, чем сейчас, если бы вы объяснили это в своем вопросе. Пожалуйста, всегда включайте все необходимые детали: D - person developer033; 15.05.2020
comment
Позвольте нам продолжить это обсуждение в чате. - person Luctia; 15.05.2020

Изменить 1:

DEMO с проверкой во вспомогательной форме (где вводятся расположен), а также на главной форме

Исходный ответ:

Для проверьте несколько полей в комбинации. Здесь у меня есть 1 флажок (начальное состояние false или не отмечено) и 3 поля ввода. В моем настраиваемом валидаторе я просто проверяю значение и устанавливаю соответствующие ошибки проверки формы:

ДЕМО

export class ProfileEditorComponent {
  myForm = new FormGroup({
    'checker': new FormControl(false),
    'name': new FormControl(),
    'middleName': new FormControl(),
    'lastName': new FormControl()
}, { validators: myCustomValidator });

  constructor(private fb: FormBuilder) { }
}

export const myCustomValidator : ValidatorFn = (control: FormGroup): ValidationErrors | null => {
  const checker = control.get('checker').value;
  const name = control.get('name');
  if (checker) {
    if (name.value===null || name.value==="") {
      return {'firstNameMissing': true};
      // TODO: do the same for the other fields or any field combination
    } else {
      return null;
    }
  }
  return null;
};

Затем сообщения об ошибках отображаются следующим образом:

  <label>
    First Name:
    <input type="text" formControlName="name">
  </label>
  <div *ngIf="myForm.errors?.firstNameMissing && (myForm.touched || myForm.dirty)" class="cross-validation-error-message alert alert-danger">
    First name required.
  </div>
person Anton    schedule 14.05.2020
comment
Это решение, кажется, приближает меня к чему-то, я думаю, мне просто не хватает одного - как я могу использовать это во вложенных FormGroups? Кажется, все работает, но в шаблоне ngIf не может читать myForm.get('subFormGroup').errors.... Я не получаю ошибок, но я вижу в журналах, что группа выдала ошибку firstNameMissing, и она не вызывает ngIf. Какие-либо предложения? - person Luctia; 14.05.2020
comment
@Luctia Итак, вы хотите, чтобы проверка выполнялась только в подгруппе. Где находится флажок - в основной или в подгруппе? - person Anton; 15.05.2020
comment
Я обновил свой вопрос, добавив лучшее представление структуры в моем FormGroup, надеюсь, вы сможете сделать вывод о том, что я имею в виду. Я уже принял ответ, поэтому уделите ему больше времени, только если вы считаете, что текущий ответ неверен. Спасибо за помощь! - person Luctia; 15.05.2020
comment
Все в порядке, теперь я понимаю, чего вы хотите достичь, и думаю, что оба решения идут в одном направлении. Удачи! - person Anton; 15.05.2020