-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Unified control state change events is awkward with template driven forms #66682
Copy link
Copy link
Open
Labels
Milestone
Description
Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
control.events is a nice way of reacting to events. However, with FormSubmittedEvent and FormResetEvent you need/want to listen the the top level control. But using control.root.events is not suitable for template driven forms as it seems at some point (after afterViewInit) the reference gets set to the correct root (before its the control itself). Funnily enough, it seems like the FormResetEvent is emitted though on the control itself. control.root.events works for reactive forms.
Example:
/** Tracks the `FormControlState` of a given control. */
export function controlState(
control: AbstractControl,
/** The element the control is associated with. */
element: HTMLElement,
options?: {
injector?: Injector;
},
): Observable<FormControlState> {
return runInInjectionContext(options?.injector ?? inject(Injector), () => {
const container = inject(ControlContainer, { optional: true });
const fieldset = element.closest('fieldset');
const form =
container?.formDirective instanceof NgForm ||
container?.formDirective instanceof FormGroupDirective
? container.formDirective
: null;
/**
* TODO: idk enough of template forms internals, but `control.root` seems to
* be updated at some point. Unfortunately, even in `afterViewInit` its
* still not updated, so there is no supported way of knowing when we have
* the actual root. It seems `container.formDirective.control.root` is the
* actual root control and works for both template and reactive forms. I
* wonder whats the correct approach of programmically getting the root
* control in Angular forms. `control.root` as a reactive property would
* help.
*
* ```ts
* console.log(control.root === control); // true even in afterViewInit
* setTimeout(() => console.log(control.root === control), 0); // false
* ```
*
* Use `control.root` when fixed.
*
* Docs:
* https://angular.dev/guide/forms/reactive-forms#unified-control-state-change-events
*/
const root = form?.control.root ?? control.root;
const getSubmitted = () => form?.submitted ?? false;
return combineLatest([
control.events.pipe(
filter((event) => !(event instanceof ValueChangeEvent)),
startWith(null),
map(
() =>
({
dirty: control.dirty,
touched: control.touched,
invalid: control.invalid,
disabled: control.disabled,
}) satisfies Partial<FormControlState>,
),
),
root.events.pipe(
filter(
(event) =>
event instanceof FormSubmittedEvent ||
event instanceof FormResetEvent,
),
map(
(event) =>
({
submitted: event instanceof FormSubmittedEvent,
}) satisfies Partial<FormControlState>,
),
startWith({
submitted: getSubmitted(),
} satisfies Partial<FormControlState>),
),
mutationObservable(element, {
attributes: true,
attributeFilter: ['readonly'],
}).pipe(
startWith(null),
map(
() =>
({
readonly: element.hasAttribute('readonly'),
}) satisfies Partial<FormControlState>,
),
),
fieldset
? mutationObservable(fieldset, {
attributes: true,
attributeFilter: ['disabled'],
}).pipe(
startWith(null),
map(
() =>
({
fieldSetDisabled: fieldset.hasAttribute('disabled'),
}) satisfies Partial<FormControlState>,
),
)
: of({
fieldSetDisabled: false,
} satisfies Partial<FormControlState>),
]).pipe(
map((formStatePartials) => ({
...formStatePartials[0],
...formStatePartials[1],
...formStatePartials[2],
...formStatePartials[3],
})),
takeUntilDestroyed(),
);
});
}Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run ng version)
20.3.10
Anything else?
No response
Reactions are currently unavailable