Skip to content

feat(side-nav): restructure component api #307 - #564

Open
ly-tempel-bitweb wants to merge 5 commits into
rcfrom
feat/307-sidenav-component-api-restructuring
Open

feat(side-nav): restructure component api #307#564
ly-tempel-bitweb wants to merge 5 commits into
rcfrom
feat/307-sidenav-component-api-restructuring

Conversation

@ly-tempel-bitweb

@ly-tempel-bitweb ly-tempel-bitweb commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

BREAKING CHANGE: The <tedi-sidenav-*> element API is removed. Wrap items in <ul tedi-sidenav-list> and use the attribute forms instead: <tedi-sidenav-item> -> <li tedi-sidenav-item> (same for dropdown, dropdown-item, group-title). Dropdown groups now use [tedi-sidenav-dropdown-group-parent] + <ul tedi-sidenav-dropdown-group-list>.

https://storybook.tedi.ee/angular/feat/307-sidenav-component-api-restructuring/?path=/docs/tedi-ready-layout-sidenav--docs

Summary by CodeRabbit

Summary

  • New Features

    • SideNav now supports semantic <ul>/<li> markup with improved navigation structure.
    • Added defaultCollapsed, defaultOpen, collapsedText, and ariaLabel options.
    • Enhanced mobile drill-down navigation with nested dropdown groups and back controls.
    • Added new SideNav list/group building blocks and dropdown-group parent behavior.
  • Bug Fixes

    • Improved overlay layering and updated overlay/background styling to use theme variables.
    • Refined dropdown, divider, focus, and collapsed layout behaviors.
  • Documentation

    • Updated SideNav documentation and examples for the new markup.
  • Tests

    • Expanded SideNav unit coverage for native DOM structure and mobile/group behavior.

BREAKING CHANGE: The <tedi-sidenav-*> element API is removed. Wrap items in
<ul tedi-sidenav-list> and use the attribute forms instead:
<tedi-sidenav-item> -> <li tedi-sidenav-item> (same for dropdown, dropdown-item,
group-title). Dropdown groups now use [tedi-sidenav-dropdown-group-parent] +
<ul tedi-sidenav-dropdown-group-list>.
@ly-tempel-bitweb ly-tempel-bitweb linked an issue Jul 24, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The SideNav now uses semantic ul/li markup, supports default-collapsed and labeled navigation states, and adds mobile group drill-down navigation. Components, styles, tests, Storybook examples, documentation, and overlay stacking were updated.

Changes

SideNav semantic composition

Layer / File(s) Summary
Semantic DOM composition
tedi/components/layout/sidenav/*
SideNav list, item, dropdown, and group components now project native list structures with updated selectors, exports, and DOM-focused tests.
Mobile drill-down and navigation state
tedi/components/layout/sidenav/sidenav.component.*, tedi/components/layout/sidenav/sidenav-item/*, tedi/components/layout/sidenav/sidenav-dropdown-group/*, tedi/services/sidenav/*, tedi/services/translation/translations.ts
Mobile group state, parent-menu navigation, defaultCollapsed, ariaLabel, defaultOpen, collapsedText, and translated back labels were added or updated.
Sidenav visual and overlay styling
tedi/components/layout/sidenav/*.scss, src/styles/cdk.scss, tedi/components/layout/header/*
Selectors, connectors, dividers, collapsed text, overlays, theme colors, and CDK overlay stacking were updated for the new structure.
Stories and semantic API documentation
skills/tedi-angular/references/components.md, tedi/components/layout/header/header.stories.ts, tedi/components/layout/sidenav/sidenav.stories.ts
Documentation and Storybook examples now use semantic attribute-form markup and cover the revised navigation states and compositions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SideNavDropdownGroupComponent
  participant SideNavService
  participant SideNavComponent
  User->>SideNavDropdownGroupComponent: Toggle non-link group
  SideNavDropdownGroupComponent->>SideNavService: setOpenGroup(group)
  SideNavService->>SideNavComponent: Update mobile group state
  User->>SideNavComponent: Select back-to-parent
  SideNavComponent->>SideNavService: handleBackToParentMenu()
  SideNavService->>SideNavDropdownGroupComponent: Close open group
Loading

Possibly related PRs

Suggested reviewers: mart-sessman

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: a side-nav API restructure to attribute-based components.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/307-sidenav-component-api-restructuring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
tedi/components/layout/sidenav/sidenav.component.scss (4)

299-312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Group-title typography duplicated verbatim.

The padding/font-size/weight/line-height/color/text-transform values here exactly repeat .tedi-sidenav-group-title__text (lines 33-41) and its &--mobile override (45-49). Extract a shared placeholder (%group-title-text or a mixin) to avoid the two blocks drifting apart on future redesigns.

♻️ Suggested extraction
+%sidenav-group-title-text {
+  padding: var(--layout-grid-gutters-08)
+    var(--navigation-vertical-item-padding-right)
+    var(--layout-grid-gutters-08)
+    var(--navigation-vertical-item-padding-left-level-1);
+  font-size: var(--body-extra-small-bold-size);
+  font-weight: var(--body-extra-small-bold-weight);
+  line-height: var(--body-extra-small-bold-line-height);
+  color: var(--navigation-vertical-group-title-text);
+  text-transform: uppercase;
+}

Then @extend %sidenav-group-title-text; in both the &--mobile .tedi-sidenav-group-title__text rule and the &-group-open ... .tedi-sidenav-dropdown-item__trigger rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/layout/sidenav/sidenav.component.scss` around lines 299 -
312, Extract the duplicated group-title typography and spacing declarations into
a shared placeholder or mixin near the existing sidenav group-title styles.
Reuse it in both the mobile `.tedi-sidenav-group-title__text` rule and the open
dropdown trigger selector `.tedi-sidenav-dropdown-group--open ...
.tedi-sidenav-dropdown-item__trigger`, preserving each rule’s intended
overrides.

233-262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Selector nesting exceeds the 4-level max.

.tedi-sidenav → &--mobile → .tedi-sidenav-dropdown-group--parent-plain → .tedi-sidenav-dropdown-group__parent--drillable → &:hover/&:focus-visible is 5 brace levels deep. Consider flattening, e.g. compound the drillable class directly with &:hover/&:focus-visible at the previous level, or extract a dedicated top-level selector for .tedi-sidenav-dropdown-group__parent--drillable.

As per coding guidelines: "Keep component SCSS nesting depth at a maximum of 4 levels."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/layout/sidenav/sidenav.component.scss` around lines 233 -
262, Flatten the hover and focus-visible selectors inside
.tedi-sidenav-dropdown-group--parent-plain so the drillable parent state does
not exceed the four-level SCSS nesting limit. Preserve the existing display,
alignment, cursor, trigger, caret, hover, focus-ring, and closed-list styles
while restructuring only the selector nesting.

Source: Coding guidelines


517-544: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tree-connector styling block duplicates lines 488-516 almost verbatim.

Trigger padding-left calc, ::before/::after dimensions and colors, and the :last-of-type::before { height: 50% } rule are repeated here identically for a different selector context. Extract a shared placeholder selector to keep both connector variants in sync.

♻️ Suggested extraction
+%sidenav-collapsed-tree-connector-neutral {
+  .tedi-sidenav-dropdown-item__trigger {
+    padding-left: calc(
+      var(--dropdown-item-padding-x) + var(--_sidenav-tree-container) +
+        var(--_gap)
+    );
+  }
+
+  &::after {
+    top: 50%;
+    width: var(--_sidenav-tree-branch-width);
+    height: var(--_sidenav-tree-trunk-width);
+    background-color: var(--navigation-vertical-tree-neutral-default);
+    transform: translateY(-50%);
+  }
+
+  &::before {
+    width: var(--_sidenav-tree-trunk-width);
+    height: 100%;
+    background-color: var(--navigation-vertical-tree-neutral-default);
+  }
+
+  &:last-of-type::before {
+    height: 50%;
+  }
+}

Then @extend %sidenav-collapsed-tree-connector-neutral; in both the .tedi-sidenav-dropdown-group__list > .tedi-sidenav-dropdown-item rule and the nested-dropdown :has() rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/layout/sidenav/sidenav.component.scss` around lines 517 -
544, Extract the duplicated tree-connector declarations from the existing
connector blocks into a shared placeholder named
%sidenav-collapsed-tree-connector-neutral, then extend it from both
.tedi-sidenav-dropdown-group__list > .tedi-sidenav-dropdown-item and the nested
.tedi-sidenav-dropdown:has(> .tedi-sidenav-dropdown-item--parent) >
.tedi-sidenav-dropdown-item:not(.tedi-sidenav-dropdown-item--parent) rule.
Preserve the current padding, pseudo-element styling, colors, and last-of-type
height behavior.

178-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Merge the sidenav hard-coded 3.5rem offsets with the shared header-height token.

--layout-header-height is already the source of truth for the header height and is reused by the header profile offset, so avoid duplicating 3.5rem in the sidenav overlay inset and the mobile min-height calculation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/layout/sidenav/sidenav.component.scss` around lines 178 -
182, Replace the hard-coded 3.5rem offset in the mobile min-height calculation
near the sidenav `&--mobile` styles with the shared `--layout-header-height`
token. Also update the sidenav overlay inset in
`tedi/components/layout/sidenav/sidenav-overlay/sidenav-overlay.component.scss`
at lines 6-9 to use that token; no other changes are needed.
tedi/components/layout/sidenav/sidenav.component.ts (1)

114-129: 📐 Maintainability & Code Quality | 🔵 Trivial

Reaching into a sibling component's private field via bracket notation.

this.sidenavService.openGroup()?.["host"]?.nativeElement accesses SideNavDropdownGroupComponent's private host field through TypeScript's bracket-notation escape hatch. It mirrors the existing handleBackToMainMenu() pattern, so it's consistent, but both are relying on an unintended TS loophole rather than a real public contract. SideNavDropdownComponent already exposes a proper element() getter for the same purpose — exposing an equivalent public accessor on SideNavDropdownGroupComponent would remove the reliance on private-field poking.
[recommended_refactor:low_effort_and_high_reward]

♻️ Suggested direction
// sidenav-dropdown-group.component.ts
+  element(): HTMLElement {
+    return this.host.nativeElement;
+  }

// sidenav.component.ts
-    const groupEl = this.sidenavService.openGroup()?.["host"]
-      ?.nativeElement as HTMLElement | undefined;
+    const groupEl = this.sidenavService.openGroup()?.element();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/layout/sidenav/sidenav.component.ts` around lines 114 - 129,
Replace the private-field bracket access in handleBackToParentMenu with a public
element accessor on SideNavDropdownGroupComponent, adding an equivalent
element() getter if needed. Use that accessor to obtain the group’s native
HTMLElement before calling sidenavService.handleBackToParentMenu, and preserve
the existing focus behavior.
tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts (1)

115-122: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Seeding defaultOpen via afterNextRender causes a visible closed→open flash on first paint.

Since dropdown is a @ContentChild, it is already available in ngAfterContentInit (which runs before the view's first render), so setting dropdown.open.set(true) there — instead of deferring to afterNextRender in ngAfterViewInit — would let the initial render already reflect the open state. Angular's own NG0100 guidance recommends ngAfterContentInit for content-derived bindings precisely to avoid this kind of post-render flip.

♻️ Suggested approach
-export class SideNavItemComponent implements AfterViewInit, OnInit, OnDestroy {
+export class SideNavItemComponent implements AfterViewInit, AfterContentInit, OnInit, OnDestroy {
   ...
+  ngAfterContentInit(): void {
+    if (this.dropdown && this.defaultOpen()) {
+      this.dropdown.open.set(true);
+    }
+  }
+
   ngAfterViewInit(): void {
     ...
-    // Seed the initial expanded state after render to avoid changing a
-    // just-checked binding during the same change-detection pass.
-    if (this.defaultOpen()) {
-      afterNextRender(() => dropdown.open.set(true), {
-        injector: this.injector,
-      });
-    }
-
     this.eventListeners.push(...)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts` around
lines 115 - 122, Move the default-open initialization from the afterNextRender
callback in ngAfterViewInit to ngAfterContentInit, setting the `@ContentChild`
dropdown.open signal before the first render. Preserve the defaultOpen condition
and remove the deferred afterNextRender logic and any now-unused dependencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/tedi-angular/references/components.md`:
- Around line 1533-1553: Update the tedi-sidenav-dropdown-group-parent anchor in
the SideNav example to use href instead of route, leaving route bindings on
supported SideNavItemComponent and SideNavDropdownItemComponent elements
unchanged.

In `@tedi/components/layout/sidenav/sidenav-composition.spec.ts`:
- Around line 85-95: Add the project’s TediTranslationService mock to the
TestBed providers in the beforeEach setup for NativeHost, alongside the existing
TEDI_TRANSLATION_DEFAULT_TOKEN provider. Keep the default locale token and
existing SideNavService configuration unchanged.

In
`@tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.html`:
- Around line 12-14: Update the caret tedi-icon in the isDrillable() block to
use color="inherit" instead of the hardcoded white color, so it follows the
parent foreground color and conforms to the component-owned icon guideline.

In `@tedi/components/layout/sidenav/sidenav.component.ts`:
- Around line 39-45: The legacy-item handling is inconsistent: hasLegacyItems is
defined but the promised wrapper is absent. Update sidenav.component.html to use
hasLegacyItems and render a tedi-sidenav-list ul around legacy projected items,
preserving direct projection for consumer-provided lists; update the related
sidenav.component.ts comment only as needed to match the implemented behavior.

---

Nitpick comments:
In `@tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts`:
- Around line 115-122: Move the default-open initialization from the
afterNextRender callback in ngAfterViewInit to ngAfterContentInit, setting the
`@ContentChild` dropdown.open signal before the first render. Preserve the
defaultOpen condition and remove the deferred afterNextRender logic and any
now-unused dependencies.

In `@tedi/components/layout/sidenav/sidenav.component.scss`:
- Around line 299-312: Extract the duplicated group-title typography and spacing
declarations into a shared placeholder or mixin near the existing sidenav
group-title styles. Reuse it in both the mobile
`.tedi-sidenav-group-title__text` rule and the open dropdown trigger selector
`.tedi-sidenav-dropdown-group--open ... .tedi-sidenav-dropdown-item__trigger`,
preserving each rule’s intended overrides.
- Around line 233-262: Flatten the hover and focus-visible selectors inside
.tedi-sidenav-dropdown-group--parent-plain so the drillable parent state does
not exceed the four-level SCSS nesting limit. Preserve the existing display,
alignment, cursor, trigger, caret, hover, focus-ring, and closed-list styles
while restructuring only the selector nesting.
- Around line 517-544: Extract the duplicated tree-connector declarations from
the existing connector blocks into a shared placeholder named
%sidenav-collapsed-tree-connector-neutral, then extend it from both
.tedi-sidenav-dropdown-group__list > .tedi-sidenav-dropdown-item and the nested
.tedi-sidenav-dropdown:has(> .tedi-sidenav-dropdown-item--parent) >
.tedi-sidenav-dropdown-item:not(.tedi-sidenav-dropdown-item--parent) rule.
Preserve the current padding, pseudo-element styling, colors, and last-of-type
height behavior.
- Around line 178-182: Replace the hard-coded 3.5rem offset in the mobile
min-height calculation near the sidenav `&--mobile` styles with the shared
`--layout-header-height` token. Also update the sidenav overlay inset in
`tedi/components/layout/sidenav/sidenav-overlay/sidenav-overlay.component.scss`
at lines 6-9 to use that token; no other changes are needed.

In `@tedi/components/layout/sidenav/sidenav.component.ts`:
- Around line 114-129: Replace the private-field bracket access in
handleBackToParentMenu with a public element accessor on
SideNavDropdownGroupComponent, adding an equivalent element() getter if needed.
Use that accessor to obtain the group’s native HTMLElement before calling
sidenavService.handleBackToParentMenu, and preserve the existing focus behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 98cc1ec7-c062-4a8b-ab5a-4b2b317e989a

📥 Commits

Reviewing files that changed from the base of the PR and between 4381b8f and 35650c1.

📒 Files selected for processing (36)
  • skills/tedi-angular/references/components.md
  • src/styles/cdk.scss
  • tedi/components/layout/header/header-profile/header-profile.component.scss
  • tedi/components/layout/header/header.stories.ts
  • tedi/components/layout/sidenav/index.ts
  • tedi/components/layout/sidenav/sidenav-composition.spec.ts
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group-list.component.ts
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group-parent.directive.ts
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.html
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.scss
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.spec.ts
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.ts
  • tedi/components/layout/sidenav/sidenav-dropdown-item/sidenav-dropdown-item.component.html
  • tedi/components/layout/sidenav/sidenav-dropdown-item/sidenav-dropdown-item.component.scss
  • tedi/components/layout/sidenav/sidenav-dropdown-item/sidenav-dropdown-item.component.spec.ts
  • tedi/components/layout/sidenav/sidenav-dropdown-item/sidenav-dropdown-item.component.ts
  • tedi/components/layout/sidenav/sidenav-dropdown/sidenav-dropdown.component.html
  • tedi/components/layout/sidenav/sidenav-dropdown/sidenav-dropdown.component.scss
  • tedi/components/layout/sidenav/sidenav-dropdown/sidenav-dropdown.component.spec.ts
  • tedi/components/layout/sidenav/sidenav-dropdown/sidenav-dropdown.component.ts
  • tedi/components/layout/sidenav/sidenav-group-title/sidenav-group-title.component.scss
  • tedi/components/layout/sidenav/sidenav-group-title/sidenav-group-title.component.ts
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.html
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.scss
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.spec.ts
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts
  • tedi/components/layout/sidenav/sidenav-list/sidenav-list.component.ts
  • tedi/components/layout/sidenav/sidenav-overlay/sidenav-overlay.component.scss
  • tedi/components/layout/sidenav/sidenav.component.html
  • tedi/components/layout/sidenav/sidenav.component.scss
  • tedi/components/layout/sidenav/sidenav.component.spec.ts
  • tedi/components/layout/sidenav/sidenav.component.ts
  • tedi/components/layout/sidenav/sidenav.stories.ts
  • tedi/services/sidenav/sidenav.service.spec.ts
  • tedi/services/sidenav/sidenav.service.ts
  • tedi/services/translation/translations.ts
💤 Files with no reviewable changes (2)
  • tedi/components/layout/sidenav/sidenav-group-title/sidenav-group-title.component.scss
  • tedi/components/layout/sidenav/sidenav-dropdown/sidenav-dropdown.component.scss

Comment thread skills/tedi-angular/references/components.md
Comment thread tedi/components/layout/sidenav/sidenav-composition.spec.ts
Comment thread tedi/components/layout/sidenav/sidenav.component.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts`:
- Around line 102-108: Remove the dropdown.open update from ngAfterContentInit;
initialize the expanded state through earlier controlled state/deep props before
rendering, or defer the update with afterNextRender/next-tick scheduling so it
does not change a checked binding during the hook.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f268e6e5-bb61-4335-8883-a4d1d1ec796d

📥 Commits

Reviewing files that changed from the base of the PR and between 35650c1 and 588789e.

📒 Files selected for processing (12)
  • skills/tedi-angular/references/components.md
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.html
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.ts
  • tedi/components/layout/sidenav/sidenav-dropdown-item/sidenav-dropdown-item.component.scss
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.scss
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.spec.ts
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts
  • tedi/components/layout/sidenav/sidenav.component.html
  • tedi/components/layout/sidenav/sidenav.component.scss
  • tedi/components/layout/sidenav/sidenav.component.spec.ts
  • tedi/components/layout/sidenav/sidenav.component.ts
  • tedi/components/layout/sidenav/sidenav.stories.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.spec.ts
  • skills/tedi-angular/references/components.md
  • tedi/components/layout/sidenav/sidenav.component.html
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.html
  • tedi/components/layout/sidenav/sidenav-dropdown-group/sidenav-dropdown-group.component.ts
  • tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.scss
  • tedi/components/layout/sidenav/sidenav.component.scss
  • tedi/components/layout/sidenav/sidenav.stories.ts

Comment on lines +102 to +108
ngAfterContentInit(): void {
// Seed the initial expanded state before the first render so the dropdown
// paints open without changing a just-checked binding.
if (this.defaultOpen() && this.dropdown) {
this.dropdown.open.set(true);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts --view expanded || true

echo "== relevant component lines =="
cat -n tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts | sed -n '1,180p'

echo "== locate dropdown component files =="
fd -a 'sidenav-dropdown|sidenav-item' . | sed 's#^\./##'

echo "== inspect dropdown signal definition if present =="
while IFS= read -r f; do
  echo "--- $f"
  rg -n "open|defaultOpen|ngAfterContentInit|ngAfterViewInit|AfterContentInit|AfterViewInit" "$f" || true
done < <(fd 'sidenav-dropdown.*\.(ts|html|scss)$' .)

Repository: TEDI-Design-System/angular

Length of output: 1938


🌐 Web query:

Angular docs ngAfterContentInit state changes ExpressionChangedAfterItHasBeenCheckedError lifecycle

💡 Result:

The ExpressionChangedAfterItHasBeenCheckedError occurs in Angular development mode when an expression in a component's template changes its value after the change detection cycle has already completed [1][2]. Angular runs an additional verification pass after the main change detection cycle to ensure that bindings remain stable; if a property used in the template is modified during this verification phase, the error is thrown to prevent inconsistent UI states [1][3]. When using the ngAfterContentInit lifecycle hook, you may encounter this error if you attempt to update component properties that are bound to your template [4]. Because ngAfterContentInit is called after Angular has initialized the projected content but during the same overall lifecycle process, modifying state here can trigger the mismatch between the initial change detection run and the subsequent verification check [5][4]. To resolve or avoid this error, consider the following strategies: 1. Asynchronous Updates: Wrap the property update in a setTimeout or use a Promise/Observable to defer the change until the next JavaScript turn [5][6]. This ensures the current change detection cycle finishes before the update occurs. 2. Refactor Timing: Move the state initialization or updates to an earlier lifecycle hook, such as ngOnInit, where it is safer to perform initial data setup [1][7]. 3. Ensure Unidirectional Data Flow: Avoid updating parent or template-bound properties from child components within lifecycle hooks that run after the view/content has been checked [1][2]. The error is specific to development mode and is designed to enforce unidirectional data flow, ensuring that data changes originate from predictable sources rather than side effects during the change detection process [1][2].

Citations:


🌐 Web query:

Angular lifecycle ngAfterContentInit after first change detection cycle signal update safe default open

💡 Result:

Updating a signal directly inside ngAfterContentInit is generally not considered safe and will typically trigger an NG0100: ExpressionChangedAfterItHasBeenCheckedError in development mode [1][2][3]. This error occurs because Angular's change detection has already completed for that phase, and modifying a value used in the view after that point violates the expected data flow [4][2]. To perform updates safely after content initialization, consider the following strategies: 1. Defer the Update: If you must update state based on content children, use afterNextRender or wrap the update in a microtask (e.g., Promise.resolve.then(...)) to ensure the update happens after the current change detection cycle has stabilized [5][6][7]. 2. Use Reactive Alternatives: If possible, avoid lifecycle hooks for state management entirely. Use computed signals to derive state reactively from your content query signals [8][9][10]. This allows Angular to manage the dependency graph automatically, preventing the error by keeping updates within the reactive system rather than imperative lifecycle methods [9][10]. 3. Evaluate Use Case: Re-examine if the state modification truly needs to happen in ngAfterContentInit. If the value is needed for initial rendering, it should ideally be handled earlier in the component lifecycle (e.g., in the constructor or ngOnInit) or via reactive derivation [5][7][2]. Effects are not recommended for propagating state or performing these types of updates, as they can also trigger NG0100 if used to modify signals directly [11][9]. In modern Angular, rely on signals and computed properties to handle derived state, and reserve lifecycle hooks or afterNextRender for genuine side effects that interact with the DOM or external systems [5][6][7].

Citations:


🌐 Web query:

Angular lifecycle ngAfterContentInit runs after first change detection phase content children bound signal

💡 Result:

In the Angular component lifecycle, the ngAfterContentInit hook is called once during the initial change detection phase, specifically after the component's projected content (content children) has been fully initialized [1][2][3]. When using legacy content queries (@ContentChild or @ContentChildren), Angular guarantees that these queries are populated and ready for access by the time ngAfterContentInit runs [4][5][6]. However, the behavior differs significantly when using modern signal-based content queries (contentChild or contentChildren) [7][8]. Because signal queries are reactive, they do not rely on the legacy ngAfterContentInit hook for initialization access [8]. If you attempt to use an effect to initialize logic based on content children accessed via signal queries, be aware of the following: 1. Timing Guarantees: Unlike the explicit timing guarantee provided by ngAfterContentInit for legacy queries, signal-based content queries do not guarantee that the initialization logic of the projected child components has already completed when the contentChildren signal is first accessed [7]. 2. Reactivity: Because signal queries update reactively, they often eliminate the need for the ngAfterContentInit lifecycle hook entirely [8]. 3. Best Practice: If your initialization logic depends on signals within the content children, ensure those dependencies are correctly tracked within a reactive context (like a computed or effect) or continue using ngOnInit in the child components to ensure their internal state is initialized before the parent component interacts with them [7]. In summary, while ngAfterContentInit remains a lifecycle event that runs after content initialization [1][3], it is increasingly superseded by signal-based reactive patterns that resolve content dependencies automatically as they change, rather than at a single, fixed point in time [8].

Citations:


Do not update dropdown.open from ngAfterContentInit.

ngAfterContentInit runs by the time the projected dropdown content is initialized, but after Angular has already checked this component’s bindings. Setting dropdown.open here can trigger ExpressionChangedAfterItHasBeenCheckedError in dev mode when defaultOpen() is true. Move the initialization to earlier controlled state/deep props before rendering, or defer it to afterNextRender/a next tick after the hook.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/layout/sidenav/sidenav-item/sidenav-item.component.ts` around
lines 102 - 108, Remove the dropdown.open update from ngAfterContentInit;
initialize the expanded state through earlier controlled state/deep props before
rendering, or defer the update with afterNextRender/next-tick scheduling so it
does not change a checked binding during the hook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SideNav]: component api restructuring

1 participant