feat(menu-layout): support contextual sidebar content
This commit is contained in:
@@ -14,6 +14,12 @@
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
|
||||
@if (sidebarContent.template(); as sidebarTemplate) {
|
||||
<div class="menu-page-layout__context">
|
||||
<ng-container [ngTemplateOutlet]="sidebarTemplate" />
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
|
||||
<main class="menu-page-layout__content">
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&__context {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: #888888;
|
||||
font-size: 0.875rem;
|
||||
|
||||
@@ -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: '<strong class="sidebar-test-badge">Componente contextual</strong>',
|
||||
})
|
||||
class SidebarTestBadgeComponent {}
|
||||
|
||||
@Component({
|
||||
imports: [MenuPageSidebarContentDirective, SidebarTestBadgeComponent],
|
||||
template: `
|
||||
<p class="routed-page-content">Contenido principal</p>
|
||||
|
||||
<ng-template menuPageSidebarContent>
|
||||
<p class="sidebar-context-copy">{{ contextualCopy }}</p>
|
||||
<app-sidebar-test-badge />
|
||||
</ng-template>
|
||||
`,
|
||||
})
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<unknown>);
|
||||
private readonly sidebarContent = inject(MenuPageSidebarContentService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sidebarContent.register(this.template);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sidebarContent.unregister(this.template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Injectable, signal, TemplateRef } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MenuPageSidebarContentService {
|
||||
private readonly templateState = signal<TemplateRef<unknown> | null>(null);
|
||||
|
||||
readonly template = this.templateState.asReadonly();
|
||||
|
||||
register(template: TemplateRef<unknown>): void {
|
||||
this.templateState.set(template);
|
||||
}
|
||||
|
||||
unregister(template: TemplateRef<unknown>): void {
|
||||
if (this.templateState() === template) {
|
||||
this.templateState.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user