Погружение в Reactive Forms

Синхронизация нескольких AbstractControl

🦊 Reactive Fox 🚀
Angular Soviet
4 min readMar 12, 2019

--

Сегодня я покажу вам простой способ, как можно синхронизировать значения не только обычных FormControl, но и FormGroup и FormArray.

В поисках проблемы

Предположим, что у нас есть форма с несколькими контролами. В которой перечислен список животных, с их кличками и цветами.

form = this.fb.array([
this.fb.group({
animal: this.fb.control(''),
name: this.fb.control(''),
color: this.fb.control('')
}),
this.fb.group({
animal: this.fb.control(''),
name: this.fb.control(''),
color: this.fb.control('')
}),
this.fb.group({
animal: this.fb.control(''),
name: this.fb.control(''),
color: this.fb.control('')
})
])

И нам необходимо синхронизировать все значения animal во всех элементах формы.

Первое, что мы можем сделать, это создать один FormControl для animal, и использовать по всей форме:

animal = this.fb.control('');form = this.fb.array([
this.fb.group({
animal: this.animal,
name: this.fb.control(''),
color: this.fb.control('')
}),
this.fb.group({
animal: this.animal,
name: this.fb.control(''),
color: this.fb.control('')
}),
this.fb.group({
animal: this.animal,
name: this.fb.control(''),
color: this.fb.control('')
})
])

Но если вы попробуете использовать такой подход, то форма будет работать неправильно. А все потому, что FormControl, как и любой другой AbstractControl, может иметь только одного родителя. Почему? Давайте просто посмотрим, что происходит в инициализации FormGroup:

class FormGroup extends AbstractControl {
constructor(
public controls: {[key: string]: AbstractControl},
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) {
super(
coerceToValidator(validatorOrOpts),
coerceToAsyncValidator(asyncValidator, validatorOrOpts));
this._initObservables();
this._setUpdateStrategy(validatorOrOpts);
this._setUpControls();
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}
_setUpControls(): void {
this._forEachChild((control: AbstractControl) => {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
});
}
}

Как мы видим, когда форма инициализируется, то каждому контролу устанавливается его родитель. Поэтому будут возникать проблемы при изменении значения animal. Что же делать?

В поисках решения

Для того, чтобы мы могли переиспользовать логику для синхронизации контролов, создадим класс ControlLinker, и реализуем в нем всю необходимую логику:

class ControlLinker<T extends AbstractControl = AbstractControl> {}

Обратите внимание на T extends AbstractControl = AbstractControl. Это необходимо для того, чтобы мы могли явно указывать тип контрола для синхронизации: FormControl, FormGroup, FormArray. А в случае, если никакой тип не указан, то будет использоваться AbstractControl как тип по-умолчанию.

Теперь, когда у нас есть класс ControlLinker, давайте добавим возможность регистрировать другие AbstractControl и помещать в список links с помощью методов link и unlink:

class ControlLinker<T extends AbstractControl> {
links: Set<ControlType> = new Set();
link(control: T) {
this.links.add(control);
}
unlink(control: T) {
this.links.delete(control);
}

}

Отлично! Теперь, когда у нас есть весь список контролов, нам необходимо добавить отслеживание их изменений. Поэтому просто сделаем это с помощью подписки на изменения значений:

link(control: T): void {
const options = { emitEvent: false };
control.valueChanges.subscribe(
(value) => this.patchValue(value, options)
);

this.links.add(control);
}
patchValue(value: any, options?: Object): void {
this.links.forEach(
(link) => link.patchValue(value, options)
);
}

И вот, что мы получили: когда придет новое значение в любом из синхронизированных контролов, то оно обработается и запишется во все имеющиеся контролы. Таким образом какой бы контрол не поменялся, все они будут иметь одинаковые значения!

Все что остается, это добавить уничтожение подписки при удалении контрола. Это можно сделать так:

subscriptions: Map<T, Subscription> = new Map<T, Subscription>();link(control: T): void {
const options = { emitEvent: false };
const subscription = control.valueChanges.subscribe(
(value) => this.patchValue(value, options)
);
this.subscriptions.add(control, subscription);
this.links.add(control);
}
unlink(control: T): void {
this.subscriptions.get(control).unsubscribe();
this.subscriptions.delete(control);
this.links.delete(control);
}

Все! Теперь мы точно уверены, что наши контролы будут синхронизированы. Давайте опробуем это в деле?

Результат

Использовать ControlLinker можно очень просто. Достаточно создать его, и передать в него все необходимые контролы. В нашем случае, передаем все поля animal:

form = this.fb.array([
this.fb.group({
animal: this.fb.control(''),
name: this.fb.control(''),
color: this.fb.control('')
}),
this.fb.group({
animal: this.fb.control(''),
name: this.fb.control(''),
color: this.fb.control('')
}),
this.fb.group({
animal: this.fb.control(''),
name: this.fb.control(''),
color: this.fb.control('')
})
])
linker = new ControlLinker<FormControl>();constructor(private fb: FormBuilder) {
const controls = this.form.controls.map(
(group) => group.get('animal')
);
controls.forEach(
(control) => this.linker.link(control)
);

}

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

Подведем итоги?

Сегодня мы разобрали простой способ синхронизации контролов реактивной формы.

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

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

И самое главное: Вы уже знаете, Почему вам НАДО отписываться?

Вы всегда можете задать мне вопрос в Телеграме. А если хочется что-то обсудить, то добро пожаловать в Телеграм-Группу.

Не забудьте подписаться на мой Twitter, GitHub, и Medium, а также обязательно сделайте 👏Clap Clap 👏 .

А еще есть Telegram канал, в котором я публикую самое интересное.

--

--

🦊 Reactive Fox 🚀
Angular Soviet

⚡️ Making Fast faster 👩‍💻 Lead Software Engineer using @angular & @dotnet 🌱 Google Developer Expert for Angular ✍ Tech Writer for @AngularInDepth 🦊 he/him