AAE-46517 Add onRowCountChange form event for Repeatable Sections (#12062)

* AAE-46517 Add onRowCountChange event for Repeatable Sections

* code review fixes for unit test
This commit is contained in:
Darren Thornton
2026-07-14 18:40:41 -05:00
committed by GitHub
parent 407a64996e
commit 4c7765e3fa
4 changed files with 139 additions and 14 deletions
@@ -16,6 +16,8 @@
*/
import { DateFnsUtils } from '../../../../common';
import { FormRulesEvent } from '../../../events/form-rules.event';
import { firstValueFrom, map, Subject, take, timeout } from 'rxjs';
import { FormFieldTypes } from './form-field-types';
import { RequiredFieldValidator } from './form-field-validator';
import { FormFieldModel } from './form-field.model';
@@ -1541,6 +1543,17 @@ describe('FormFieldModel', () => {
});
describe('add row', () => {
const assignFormRulesEventSubject = (): Subject<FormRulesEvent> => {
const formRulesEvent = new Subject<FormRulesEvent>();
(field.form as any).formService = {
formRulesEvent,
validateForm: new Subject(),
validateFormField: new Subject(),
formFieldValueChanged: new Subject()
};
return formRulesEvent;
};
it('should add row if allowed by limit param', () => {
expect(field.rows.length).toBe(2);
@@ -1568,18 +1581,56 @@ describe('FormFieldModel', () => {
expect(field.rows.length).toBe(5);
});
it('should call onRepeatableSectionChanged', () => {
spyOn(field.form, 'onRepeatableSectionChanged').and.callThrough();
it('should call onRepeatableSectionRowCountChanged', () => {
spyOn(field.form, 'onRepeatableSectionRowCountChanged').and.callThrough();
expect(field.form.onRepeatableSectionChanged).not.toHaveBeenCalled();
expect(field.form.onRepeatableSectionRowCountChanged).not.toHaveBeenCalled();
field.addRow(field.fields, form);
expect(field.form.onRepeatableSectionChanged).toHaveBeenCalled();
expect(field.form.onRepeatableSectionRowCountChanged).toHaveBeenCalledWith(field);
});
it('should emit onRowCountChanged via formService.formRulesEvent', async () => {
const formRulesEvent = assignFormRulesEventSubject();
const emissionPromise = firstValueFrom(formRulesEvent);
field.addRow(field.fields, form);
const emittedEvent = await emissionPromise;
expect(emittedEvent.type).toBe('onRowCountChanged');
expect(emittedEvent.field).toBe(field);
});
it('should NOT emit onRowCountChanged when add row is not allowed', async () => {
const formRulesEvent = assignFormRulesEventSubject();
field.addRow(field.fields, form);
field.addRow(field.fields, form);
field.addRow(field.fields, form);
expect(field.rows.length).toBe(5);
const emissionPromise = firstValueFrom(formRulesEvent.pipe(timeout(50)));
field.addRow(field.fields, form);
await expectAsync(emissionPromise).toBeRejected();
expect(field.rows.length).toBe(5);
});
});
describe('remove row', () => {
const assignFormRulesEventSubject = (): Subject<FormRulesEvent> => {
const formRulesEvent = new Subject<FormRulesEvent>();
(field.form as any).formService = {
formRulesEvent,
validateForm: new Subject(),
validateFormField: new Subject(),
formFieldValueChanged: new Subject()
};
return formRulesEvent;
};
it('should remove row if target index exists', () => {
expect(field.rows.length).toBe(2);
@@ -1646,14 +1697,77 @@ describe('FormFieldModel', () => {
expect(field.form.onFormFieldChanged).not.toHaveBeenCalled();
});
it('should call onRepeatableSectionChanged', () => {
spyOn(field.form, 'onRepeatableSectionChanged').and.callThrough();
it('should call onRepeatableSectionRowCountChanged', () => {
spyOn(field.form, 'onRepeatableSectionRowCountChanged').and.callThrough();
expect(field.form.onRepeatableSectionChanged).not.toHaveBeenCalled();
expect(field.form.onRepeatableSectionRowCountChanged).not.toHaveBeenCalled();
field.removeRow(1);
expect(field.form.onRepeatableSectionChanged).toHaveBeenCalled();
expect(field.form.onRepeatableSectionRowCountChanged).toHaveBeenCalledWith(field);
});
it('should emit onRowCountChanged via formService.formRulesEvent', async () => {
const formRulesEvent = assignFormRulesEventSubject();
const emissionPromise = firstValueFrom(formRulesEvent);
field.removeRow(1);
const emittedEvent = await emissionPromise;
expect(emittedEvent.type).toBe('onRowCountChanged');
expect(emittedEvent.field).toBe(field);
});
it('should update form values before emitting onRowCountChanged', async () => {
const formRulesEvent = assignFormRulesEventSubject();
form.values[field.id] = [
{
Text0wwp7n: 'mock-1',
Integer0rzkwq: 1
},
{
Text0wwp7n: 'mock-2',
Integer0rzkwq: 2
}
];
const valuesLengthAtEmissionPromise = firstValueFrom(
formRulesEvent.pipe(
take(1),
map(() => form.values[field.id].length)
)
);
field.removeRow(1);
const valuesLengthAtEmission = await valuesLengthAtEmissionPromise;
expect(valuesLengthAtEmission).toBe(1);
expect(form.values[field.id]).toEqual([
{
Text0wwp7n: 'mock-1',
Integer0rzkwq: 1
}
]);
});
it('should emit onRowCountChanged even when form values do not contain section id', async () => {
const formRulesEvent = assignFormRulesEventSubject();
form.values = {};
const emissionPromise = firstValueFrom(formRulesEvent);
field.removeRow(1);
const emittedEvent = await emissionPromise;
expect(emittedEvent.type).toBe('onRowCountChanged');
});
it('should NOT emit onRowCountChanged when remove row target index does not exist', async () => {
const formRulesEvent = assignFormRulesEventSubject();
const emissionPromise = firstValueFrom(formRulesEvent.pipe(timeout(50)));
field.removeRow(2);
await expectAsync(emissionPromise).toBeRejected();
});
});
@@ -470,7 +470,7 @@ export class FormFieldModel extends FormWidgetModel {
}
this.rows.push(this.createRow(fields, form, this.rows.length));
this.form.onRepeatableSectionChanged();
this.form.onRepeatableSectionRowCountChanged(this);
}
private shouldAddRow(): boolean {
@@ -484,14 +484,17 @@ export class FormFieldModel extends FormWidgetModel {
this.rows.splice(index, 1);
this.updateChildrenFieldsRowIndex();
this.form.onRepeatableSectionChanged();
if (!this.form.values[this.id]) {
return;
const hasSectionValues = !!this.form.values[this.id];
if (hasSectionValues) {
this.form.values[this.id].splice(index, 1);
}
this.form.values[this.id].splice(index, 1);
this.form.onFormFieldChanged(this);
this.form.onRepeatableSectionRowCountChanged(this);
if (hasSectionValues) {
this.form.onFormFieldChanged(this);
}
}
private shouldRemoveRow(index: number): boolean {
@@ -16,6 +16,7 @@
*/
import { FormFieldEvent } from '../../../events/form-field.event';
import { FormRulesEvent } from '../../../events/form-rules.event';
import { ValidateFormFieldEvent } from '../../../events/validate-form-field.event';
import { ValidateFormEvent } from '../../../events/validate-form.event';
import { ContainerModel } from './container.model';
@@ -154,6 +155,11 @@ export class FormModel implements ProcessFormModel {
this.fieldsCache = this.getFormFields([], true);
}
onRepeatableSectionRowCountChanged(sectionField: FormFieldModel): void {
this.onRepeatableSectionChanged();
this.formService?.formRulesEvent?.next(new FormRulesEvent('onRowCountChanged', new FormFieldEvent(this, sectionField)));
}
/**
* Validates entire form and all form fields.
*/
@@ -17,6 +17,7 @@
import { Subject } from 'rxjs';
import { FormFieldEvent } from '../events/form-field.event';
import { FormRulesEvent } from '../events/form-rules.event';
import { ValidateFormFieldEvent } from '../events/validate-form-field.event';
import { ValidateFormEvent } from '../events/validate-form.event';
@@ -24,4 +25,5 @@ export interface FormValidationService {
formFieldValueChanged: Subject<FormFieldEvent>;
validateForm: Subject<ValidateFormEvent>;
validateFormField: Subject<ValidateFormFieldEvent>;
formRulesEvent?: Subject<FormRulesEvent>;
}