From 7d27d77629a7d17e4dae1cd3c284a65d4791c9d7 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 22 Sep 2026 13:54:19 -0300 Subject: [PATCH] feat(menu-layout): support contextual sidebar content --- .../menu-page-layout.component.html | 6 ++ .../menu-page-layout.component.scss | 4 ++ .../menu-page-layout.component.spec.ts | 65 ++++++++++++++++++- .../menu-page-layout.component.ts | 6 +- .../menu-page-sidebar-content.directive.ts | 24 +++++++ .../menu-page-sidebar-content.service.ts | 18 +++++ 6 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.directive.ts create mode 100644 src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.service.ts diff --git a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.html b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.html index 7ba9834..5ca1fe1 100644 --- a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.html +++ b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.html @@ -14,6 +14,12 @@ } + + @if (sidebarContent.template(); as sidebarTemplate) { + + }
diff --git a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.scss b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.scss index 3db8125..b868e3c 100644 --- a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.scss +++ b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.scss @@ -40,6 +40,10 @@ gap: 1rem; } + &__context { + margin-top: 2rem; + } + &__link { color: #888888; font-size: 0.875rem; diff --git a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.spec.ts b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.spec.ts index 051c8bb..0c94ba6 100644 --- a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.spec.ts +++ b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.spec.ts @@ -1,11 +1,34 @@ -import { signal } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { ActivatedRoute, provideRouter } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; import { describe, expect, it } from 'vitest'; import { Tenant } from '../../../../core/services/tenant.interface'; import { TenantService } from '../../../../core/services/tenant.service'; import { MenuPageLayoutComponent } from './menu-page-layout.component'; +import { MenuPageSidebarContentDirective } from './menu-page-sidebar-content.directive'; + +@Component({ + selector: 'app-sidebar-test-badge', + template: 'Componente contextual', +}) +class SidebarTestBadgeComponent {} + +@Component({ + imports: [MenuPageSidebarContentDirective, SidebarTestBadgeComponent], + template: ` +

Contenido principal

+ + + + + + `, +}) +class SidebarTestPageComponent { + protected readonly contextualCopy = 'HTML declarado por la página'; +} function createTenant(): Tenant { return { @@ -133,4 +156,44 @@ describe('MenuPageLayoutComponent', () => { 'Consultas habituales', ); }); + + it('renders page-owned HTML and components below the sidebar navigation', async () => { + const tenant = signal(createTenant()); + + await TestBed.configureTestingModule({ + providers: [ + provideRouter([ + { + path: 'mi-cuenta', + component: MenuPageLayoutComponent, + data: { + menuCode: 'account', + navigationLabel: 'Secciones de mi cuenta', + }, + children: [{ path: 'tickets', component: SidebarTestPageComponent }], + }, + ]), + { + provide: TenantService, + useValue: { tenant: tenant.asReadonly() }, + }, + ], + }).compileComponents(); + + const harness = await RouterTestingHarness.create('/mi-cuenta/tickets'); + const element = harness.routeNativeElement as HTMLElement; + const navigation = element.querySelector('.menu-page-layout__nav'); + const context = element.querySelector('.menu-page-layout__context'); + + expect(context?.previousElementSibling).toBe(navigation); + expect(context?.querySelector('.sidebar-context-copy')?.textContent?.trim()).toBe( + 'HTML declarado por la página', + ); + expect(context?.querySelector('.sidebar-test-badge')?.textContent?.trim()).toBe( + 'Componente contextual', + ); + expect(element.querySelector('.routed-page-content')?.textContent?.trim()).toBe( + 'Contenido principal', + ); + }); }); diff --git a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.ts b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.ts index 0640f32..4818432 100644 --- a/src/app/features/store/components/menu-page-layout/menu-page-layout.component.ts +++ b/src/app/features/store/components/menu-page-layout/menu-page-layout.component.ts @@ -1,18 +1,22 @@ +import { NgTemplateOutlet } from '@angular/common'; import { Component, computed, inject } from '@angular/core'; import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { findMenu } from '../../../../core/services/menu.utils'; import { TenantService } from '../../../../core/services/tenant.service'; +import { MenuPageSidebarContentService } from './menu-page-sidebar-content.service'; @Component({ selector: 'app-menu-page-layout', - imports: [RouterLink, RouterLinkActive, RouterOutlet], + imports: [NgTemplateOutlet, RouterLink, RouterLinkActive, RouterOutlet], + providers: [MenuPageSidebarContentService], templateUrl: './menu-page-layout.component.html', styleUrl: './menu-page-layout.component.scss', }) export class MenuPageLayoutComponent { private readonly route = inject(ActivatedRoute); private readonly tenantService = inject(TenantService); + protected readonly sidebarContent = inject(MenuPageSidebarContentService); protected readonly navigationLabel = (this.route.snapshot.data['navigationLabel'] as string | undefined) ?? 'Secciones'; diff --git a/src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.directive.ts b/src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.directive.ts new file mode 100644 index 0000000..3a1ac91 --- /dev/null +++ b/src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.directive.ts @@ -0,0 +1,24 @@ +import { Directive, inject, OnDestroy, OnInit, TemplateRef } from '@angular/core'; + +import { MenuPageSidebarContentService } from './menu-page-sidebar-content.service'; + +/** + * Projects page-owned content into the sidebar of the closest menu page layout. + * The template keeps the declaration context of the routed page, so it can use + * that page's fields, bindings and imported components. + */ +@Directive({ + selector: 'ng-template[menuPageSidebarContent]', +}) +export class MenuPageSidebarContentDirective implements OnInit, OnDestroy { + private readonly template = inject(TemplateRef); + private readonly sidebarContent = inject(MenuPageSidebarContentService); + + ngOnInit(): void { + this.sidebarContent.register(this.template); + } + + ngOnDestroy(): void { + this.sidebarContent.unregister(this.template); + } +} diff --git a/src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.service.ts b/src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.service.ts new file mode 100644 index 0000000..e5f10ac --- /dev/null +++ b/src/app/features/store/components/menu-page-layout/menu-page-sidebar-content.service.ts @@ -0,0 +1,18 @@ +import { Injectable, signal, TemplateRef } from '@angular/core'; + +@Injectable() +export class MenuPageSidebarContentService { + private readonly templateState = signal | null>(null); + + readonly template = this.templateState.asReadonly(); + + register(template: TemplateRef): void { + this.templateState.set(template); + } + + unregister(template: TemplateRef): void { + if (this.templateState() === template) { + this.templateState.set(null); + } + } +}