Skip to content

feat(router): handle null and undefined inputs in RouterLinkActive#68120

Open
arturovt wants to merge 1 commit intoangular:mainfrom
arturovt:feat/router-issue-66233
Open

feat(router): handle null and undefined inputs in RouterLinkActive#68120
arturovt wants to merge 1 commit intoangular:mainfrom
arturovt:feat/router-issue-66233

Conversation

@arturovt
Copy link
Copy Markdown
Contributor

@arturovt arturovt commented Apr 10, 2026

Without this change, components that use RouterLinkActive in multiple
contexts (e.g. both a navigation menu and body content) are forced to
branch the template for every conditional input:

  @if (activeClass) {
    <a [routerLink]="href" [routerLinkActive]="activeClass"
       [routerLinkActiveOptions]="activeOptions"
       [ariaCurrentWhenActive]="ariaCurrent">
      <ng-content />
    </a>
  } @else {
    <a [routerLink]="href"><ng-content /></a>
  }

Every additional input multiplies the branching, and each @if/@else
injects unwanted comment nodes into the DOM. There is no way to
conditionally attach a directive in Angular templates, making imperative
TypeScript instantiation the only alternative.

Accepting null/undefined collapses this to a single template branch:

  <a [routerLink]="href"
     [routerLinkActive]="activeClass"
     [routerLinkActiveOptions]="activeOptions"
     [ariaCurrentWhenActive]="ariaCurrent">
    <ng-content />
  </a>

When activeClass is undefined (e.g. in content areas), the directive
stays mounted but applies no CSS classes. When it is a string (e.g. in
the navigation), normal active-class behavior applies — no branching, no
extra DOM nodes, no TypeScript workarounds.

  • routerLinkActive: null/undefined now sets an empty class list.

  • routerLinkActiveOptions: null and undefined are treated differently:

    • undefined → falls back to the default subset match ("not set")
    • null → explicit opt-out, link is never considered active

Closes #66233

@pullapprove pullapprove bot requested a review from thePunderWoman April 10, 2026 14:34
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: router labels Apr 10, 2026
@ngbot ngbot bot added this to the Backlog milestone Apr 10, 2026
@JeanMeche JeanMeche requested review from atscott and removed request for thePunderWoman April 10, 2026 14:38
Copy link
Copy Markdown
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please also add your use-case to the issue report? This change was rejected the first time due to very low signal of community need in the report.

*
* These options are passed to the `isActive()` function.
*
* When `null` or `undefined`, the default subset match behavior is used.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation doesn't reflect the PR description (or implementation)

Comment thread packages/router/src/directives/router_link_active.ts Outdated
Comment thread packages/router/src/directives/router_link_active.ts Outdated
Comment thread packages/router/src/directives/router_link_active.ts Outdated
Comment thread packages/router/src/directives/router_link_active.ts Outdated
Comment thread packages/router/src/directives/router_link_active.ts Outdated
@arturovt arturovt force-pushed the feat/router-issue-66233 branch from 63e937b to f1d270e Compare April 10, 2026 19:06
@arturovt
Copy link
Copy Markdown
Contributor Author

@atscott I just met the same situation recently as described in the issue, I've updated commit description.

@atscott
Copy link
Copy Markdown
Contributor

atscott commented Apr 11, 2026

  @if (activeClass) {
    <a [routerLink]="href" [routerLinkActive]="activeClass"
       [routerLinkActiveOptions]="activeOptions"
       [ariaCurrentWhenActive]="ariaCurrent">
      <ng-content />
    </a>
  } @else {
    <a [routerLink]="href"><ng-content /></a>
  }

that’s not really true is it? Why not this:

    <a [routerLink]="href" [routerLinkActive]="activeClass ?? []"
       [routerLinkActiveOptions]="activeOptions ?? {}"
       [ariaCurrentWhenActive]="ariaCurrent">
      <ng-content />
    </a>

That seems like quite a small thing to have to do.

Without this change, components that use RouterLinkActive in multiple
contexts (e.g. both a navigation menu and body content) are forced to
branch the template for every conditional input:

  @if (activeClass) {
    <a [routerLink]="href" [routerLinkActive]="activeClass"
       [routerLinkActiveOptions]="activeOptions"
       [ariaCurrentWhenActive]="ariaCurrent">
      <ng-content />
    </a>
  } @else {
    <a [routerLink]="href"><ng-content /></a>
  }

Every additional input multiplies the branching, and each @if/@else
injects unwanted comment nodes into the DOM. There is no way to
conditionally attach a directive in Angular templates, making imperative
TypeScript instantiation the only alternative.

Accepting null/undefined collapses this to a single template branch:

  <a [routerLink]="href"
     [routerLinkActive]="activeClass"
     [routerLinkActiveOptions]="activeOptions"
     [ariaCurrentWhenActive]="ariaCurrent">
    <ng-content />
  </a>

When activeClass is undefined (e.g. in content areas), the directive
stays mounted but applies no CSS classes. When it is a string (e.g. in
the navigation), normal active-class behavior applies — no branching, no
extra DOM nodes, no TypeScript workarounds.

- `routerLinkActive`: null/undefined now sets an empty class list.

- `routerLinkActiveOptions`: null and undefined are treated differently:
  - undefined → falls back to the default subset match ("not set")
  - null → explicit opt-out, link is never considered active

Closes angular#66233
@arturovt arturovt force-pushed the feat/router-issue-66233 branch from f1d270e to d085ebe Compare April 11, 2026 14:15
@arturovt
Copy link
Copy Markdown
Contributor Author

arturovt commented Apr 11, 2026

@atscott I have also updated the code with a return-guard in update().

1. routerLink already treats null as “turn this off”

routerLink explicitly treats null/undefined as a way to disable the directive:

// null|undefined: effectively disables the routerLink
set routerLink(commandsOrUrlTree: readonly any[] | string | UrlTree | null | undefined) {
  if (commandsOrUrlTree == null) {
    this.routerLinkInput.set(null);
    ...
  }
}

routerLinkActive is typically used alongside routerLink on the same element, so it’s natural to expect the same behavior. Using null to mean “opt out” is already the established pattern here. Having routerLinkActive behave differently creates an unnecessary inconsistency.

2. ?? [] and ?? {} aren’t real substitutes

The common workaround (activeClass ?? [] or activeOptions ?? {}) doesn’t actually disable the directive:

  • activeClass ?? [] passes an empty array. The directive still subscribes to router events, runs matching logic, and triggers updates — it just doesn’t apply classes.
  • activeOptions ?? {} passes an actual object, not “no options”. An empty object is still a value with meaning, not the absence of intent.

Using null clearly expresses “I don’t want this active,” whereas [] or {} still activates the directive with a specific behavior.

3. Strict template type-checking makes this worse

With strict inputs enabled, a component input like:

@Input() activeClass: string | null;

won’t work with:

<a [routerLinkActive]="activeClass">

Every consumer now has to remember to write activeClass ?? [] everywhere. That pushes the burden onto the caller instead of handling it once inside the directive.

Summary

The ?? workaround technically works, but it:

  • Forces every caller to work around the same limitation
  • Keeps the directive running even when it shouldn’t
  • Introduces tricky issues with content projection in wrapper components
  • Breaks consistency with how routerLink already behaves

Letting routerLinkActive accept null would be a cleaner, more intuitive API and would match the behavior developers already expect from routerLink.


Angular directives broadly accept null | undefined on inputs to mean "this input is not set". routerLink does it. ngClass does it. ngStyle does it. This is not a new pattern — it's the established contract for optional inputs across the framework. routerLinkActive should be consistent with that, not an exception.

@thePunderWoman
Copy link
Copy Markdown
Contributor

Woah, looks like you've opened a lot of issues/PRs recently. While we appreciate contributions from the community, triaging and reviewing a large influx of content in a short time period takes time away from other ongoing projects. As a result, we're closing these issues/PRs to maintain the team's focus.

Note that this is not necessarily a rejection of the goals or direction of any of these contributions in particular, so much as a reflection of the team's current capacity and priorities.

You are welcome to open a smaller subset of issues/PRs in accordance with our policy focused on the most important and impactful contributions and we will do our best to prioritize a response as soon as possible.

@atscott atscott reopened this Apr 13, 2026
@atscott atscott added the target: minor This PR is targeted for the next minor release label Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: router detected: feature PR contains a feature commit target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RouterLinkActive: Handle undefined/null

3 participants