what could be preventing an input field's focus ev...
Created on: June 17, 2025
Created on: June 17, 2025
what could be preventing an input field's focus event from firing? I click the field, it clearly receives focus as I can start typing but the focus event is not firing. If I blur the field, the next focusing attempt triggers the focus event.
<input type="search" class="text-field" name="autocomplete" [ngClass]="{ 'autocomplete__input--is-active': isVisible }" [value]="inputValue" (input)="onInput(event)" />
There is no event.preventDefault or event.stopPropagation anywhere in the event handlers.
When an input plainly gets the caret yet your (focus)
handler never runs, usually one of just a handful of things is happening behind the scenes:
Symptom | Behind-the-scenes cause | How to confirm / fix |
---|---|---|
1. The element was already focused before you clicked it (e.g. code or an autofocus directive called .focus() in ngAfterViewInit , a library such as MatAutocompleteTrigger , or a keyboard shortcut that shifts focus). Because it never lost focus, the native focus event does not fire when you click. | - Put console.log(document.activeElement) in ngAfterViewInit() or tap the page with dev-tools open.<br>- Remove the programmatic .focus() (or the autofocus ) or add your own blur first. | |
2. Another listener stops the event before your handler sees it. focus does not bubble, so if some directive attached to the same element calls event.stopImmediatePropagation() , subsequent listeners (including the one Angular attached for your template) are skipped. Material, PrimeNG & other component libs sometimes do this. | - Add a plain listener in the console: el.addEventListener('focus', () => console.log('raw focus'), true) . If that prints while your Angular callback does not, something is stopping it.<br>- Replace (focus) with (focusin) (which bubbles) or move your handler into a directive that runs first. | |
3. The element is destroyed and recreated on the same click (e.g. hidden by an *ngIf , ngSwitch , or route change). The old element gets focus and fires its event, then is torn down before Angular change-detection reaches your callback, so you never see it. | - Give the element a #ref and in ngAfterViewChecked log ref ’s isConnected after the click.<br>- Instead of conditionally removing the input, hide it with CSS ([hidden] /[style.display] ) or delay the state change with setTimeout(() => { … }) . | |
4. You’re really after the bubbling version of the event. In plain DOM, focus /blur don’t bubble; the bubbling equivalents are focusin /focusout . Angular wires (focus) directly to focus . If your markup is inside a child component, its own (focus) may catch the event and you never see it at the parent level. | - Try (focusin)="onFocusInput()" instead. |
.focus()
on that input (directives, third-party components, tests, your own code).addEventListener
) with useCapture=true
to verify whether the event is fired at all.(focus)
for (focusin)
or (focusout)
if you really need bubbling.onClick()
, make sure it doesn’t tear down the very input you just clicked.Once you remove (or account for) the earlier programmatic focus or intercepting listener, your onFocusInput()
handler will fire the first time you interact with the field again.
const option = new FormControl<string>('', [Validators.required])
why is option inferred as FormControl<string | null>? Where does that null come from?
FormControl
is typed so that a control’s value can always become null
at runtime (for example when you call reset()
or when Angular internally clears a control).
Because of that, the class is declared roughly like this (simplified):
tsexport class FormControl<T = any> extends AbstractControl { /** the public value */ readonly value!: T | null; constructor( formState?: T | FormControlState<T> | null, opts?: FormControlOptions // <- has `nonNullable?` ) { … } }
Notice that the public value
property is T | null
no matter what T
is.
So when the compiler sees
tsconst option = new FormControl<string>('', [Validators.required]);
it infers
tsconst option: FormControl<string | null>;
because the class definition itself adds the | null
. It’s not your argument that adds it; it’s the way the control is declared in the Angular typings. The Angular docs even call this out explicitly: “You might notice that null
is always added to the type of the control. This is because the control will become null
if you call reset
.” v17.angular.io
Angular gives you an escape hatch: set the nonNullable
flag (or use NonNullableFormBuilder
). Then the class picks a different constructor overload whose value
property is just T
.
tsconst option = new FormControl<string>( '', { validators: [Validators.required], nonNullable: true } ); // ^-- now inferred as FormControl<string>
or with the builder:
tsfb.nonNullable.control('', Validators.required);
With nonNullable: true
the control is reset back to its initial value instead of null
, and option.value
is always string
. The docs and many answers (e.g. Stack Overflow) show this pattern as the recommended way when you know the control should never be null
. stackoverflow.com
Got it, thanks!
I have this:
interface KycAgreementData {
option?: string;
email?: string;
number?: string;
}
type KycAgreementDataControls = {
[k in keyof KycAgreementData]: FormControl<string | undefined>;
};
And the form is built with:
this.agreementForm = new FormGroup<KycAgreementDataControls>({
option: new FormControl<string>('', { validators: [Validators.required], nonNullable: true }),
email: new FormControl<string>('', { validators: [], nonNullable: true }),
number: new FormControl<string>('', { validators: [Validators.required], nonNullable: true }),
});
However, this.agreementForm.controls.email is inferred as FormControl<string | undefined> | undefined.
What does it thing the email control can be also undefined?
agreementForm.controls
gets its type from the KycAgreementDataControls
alias you fed to FormGroup
.
That alias preserves the optional-ness of the original properties — and that is where the extra | undefined
on controls.email
comes from.
tsinterface KycAgreementData { option?: string; // optional email?: string; // optional number?: string; // optional } type KycAgreementDataControls = { // the `?` modifier is copied verbatim [K in keyof KycAgreementData]: FormControl<string | undefined>; // ^ ^ // | property is still optional // +-------------- value can be undefined (your own choice) };
Because email
is optional in KycAgreementData
, the mapped type keeps the ?
.
So KycAgreementDataControls
is effectively
ts{ option?: FormControl<string | undefined>; email?: FormControl<string | undefined>; number?: FormControl<string | undefined>; }
FormGroup<T>
just reflects that shape in its controls
property:
tsclass FormGroup<T extends { [P: string]: AbstractControl }> { readonly controls!: T; }
Therefore agreementForm.controls.email
is typed as
tsFormControl<string | undefined> | undefined ^ ^ | \-- property itself may be absent \-- your own union in the mapped type
At runtime you did supply the control, but the type system can’t assume that because email
is allowed to be missing by the interface.
Choose the option that best matches the shape you really want:
?
from the source interfacetsinterface KycAgreementData { option: string; email: string; number: string; }
tstype KycAgreementDataControls = { [K in keyof KycAgreementData]-?: FormControl<string>; // <- note the -? };
The -?
removes the optional modifier.
Required<>
when you build the formtsthis.agreementForm = new FormGroup<Required<KycAgreementDataControls>>({ … });
You already pass { nonNullable: true }
when you create each control, so you can drop the | undefined
inside the control’s value type:
tstype KycAgreementDataControls = { [K in keyof KycAgreementData]-?: FormControl<string>; // ^ no more | undefined };
With these tweaks the compiler will infer:
tsagreementForm.controls.email // FormControl<string>
— no extra | undefined
anywhere.