File
Implements
Index
Properties
|
|
Methods
|
|
Inputs
|
|
HostBindings
|
|
Accessors
|
|
chromeless
|
Type : boolean
|
Default value : false
|
|
headerLogoLinkUrl
|
Type : string
|
|
offerCodeStateKey
|
Type : string
|
|
sidebar
|
Type : boolean
|
Default value : false
|
|
spinner
|
Type : boolean
|
Default value : false
|
|
HostBindings
class.collapsed-navigation
|
Type : boolean
|
Default value : false
|
|
class.has-navigation
|
Type : boolean
|
|
class.has-stage
|
Type : boolean
|
Default value : false
|
|
class.is-stacked
|
Type : boolean
|
Default value : false
|
|
class.no-header
|
Type : any
|
|
class.use-enhanced-vertical-spacing
|
Type : any
|
Default value : inject(USE_ENHANCED_VERTICAL_SPACING)
|
|
Methods
disableBackgroundScrolling
|
disableBackgroundScrolling()
|
|
|
onResize
|
onResize(value: number)
|
|
Parameters :
Name |
Type |
Optional |
value |
number
|
No
|
|
_hasStage
|
Default value : false
|
Decorators :
@HostBinding('class.has-stage')
|
|
collapsedNavigation
|
Default value : false
|
Decorators :
@HostBinding('class.collapsed-navigation')
|
|
frameLayoutService
|
Default value : inject(TalyFrameLayoutService)
|
|
frameNavigationService
|
Default value : inject(TalyFrameNavigationService)
|
|
hasBannerContent
|
Default value : false
|
|
hasJumpNavigationMenu
|
Default value : false
|
|
hasOfferCodeContent
|
Default value : false
|
|
hasSmallPrintContent
|
Default value : false
|
|
hasStage$
|
Type : Observable<boolean>
|
|
Public
Optional
headerTemplate
|
Type : TemplateRef<>
|
Decorators :
@ContentChild('headerTemplate', {static: true, read: TemplateRef})
|
|
sidebarService
|
Default value : inject(TalyFrameSidebarService)
|
|
stackedLayout
|
Default value : false
|
Decorators :
@HostBinding('class.is-stacked')
|
|
useEnhancedVerticalSpacing
|
Default value : inject(USE_ENHANCED_VERTICAL_SPACING)
|
Decorators :
@HostBinding('class.use-enhanced-vertical-spacing')
|
|
Accessors
isHeaderHidden
|
getisHeaderHidden()
|
|
hasOfferCode
|
gethasOfferCode()
|
|
centered
|
getcentered()
|
|
setcentered(value: boolean)
|
|
Parameters :
Name |
Type |
Optional |
value |
boolean
|
No
|
|
hasNavigation
|
gethasNavigation()
|
|
import { AclService } from '@allianz/taly-acl/angular';
import { WEB_COMPONENT_ID } from '@allianz/taly-common/web-components';
import {
CHANNEL,
CHANNEL_TOKEN,
NO_MARGIN,
TalyPageDataService,
TalyStickyService,
USE_ENHANCED_VERTICAL_SPACING
} from '@allianz/taly-core';
import { FooterConfiguration, StageConfiguration } from '@allianz/taly-core/schemas';
import {
AfterViewInit,
Component,
ContentChild,
DestroyRef,
ElementRef,
HostBinding,
inject,
Input,
NgZone,
OnChanges,
OnDestroy,
OnInit,
Renderer2,
SimpleChanges,
TemplateRef,
ViewChild
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, skip, take, type Observable } from 'rxjs';
import { NavigationConfig } from '../frame-parts/navigation/model';
import { TalyFrameBannerService } from '../services/banner.service';
import { TalyFrameLayoutService } from '../services/frame-layout.service';
import { TalyFrameSidebarService } from '../services/sidebar.service';
import { TalyFrameNavigationService } from '../services/taly-frame-navigation-service';
import { TalyFrameSmallPrintService } from '../services/small-print.service';
@Component({
selector: 'taly-frame',
templateUrl: './frame.component.html',
styleUrls: ['./frame.component.scss'],
standalone: false
})
export class FrameComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
frameLayoutService = inject(TalyFrameLayoutService);
sidebarService = inject(TalyFrameSidebarService);
private pageDataService = inject(TalyPageDataService);
frameNavigationService = inject(TalyFrameNavigationService);
private smallPrintService = inject(TalyFrameSmallPrintService);
private el = inject(ElementRef);
private renderer = inject(Renderer2);
private pageId = '';
@Input()
chromeless = false;
@HostBinding('class.no-header')
get isHeaderHidden() {
return this.chromeless || this.aclService.isHidden('taly-frame-header');
}
@HostBinding('class.has-banner')
get hasBanner() {
return this.hasBannerContent && !this.aclService.isHidden(this.pageId + '/banner');
}
get hasFooter() {
return !this.chromeless && !this.aclService.isHidden('taly-frame-footer');
}
get hasOfferCode() {
return this.hasOfferCodeContent && !this.aclService.isHidden(this.pageId + '/offer-code');
}
@HostBinding('class.has-header')
get hasHeader() {
return !this.isHeaderHidden;
}
@Input() title?: string;
@Input() logoSrc?: string;
private _centered = false;
@Input()
@HostBinding('class.is-centered')
set centered(value: boolean) {
this._centered = value;
}
get centered() {
return this._centered || !this.hasNavigation;
}
@HostBinding('class.has-stage')
_hasStage = false;
hasSmallPrintContent = false;
hasOfferCodeContent = false;
@HostBinding('class.is-stacked')
stackedLayout = false;
@HostBinding('class.collapsed-navigation')
collapsedNavigation = false;
@Input()
@HostBinding('class.has-sidebar')
sidebar = false;
@HostBinding('class.has-navigation')
get hasNavigation() {
if (
this.navigationConfig?.sections === undefined ||
this.navigationConfig?.sections?.size === 0 ||
this.pageDataService.pageData?.navigation?.hidden
) {
return false;
}
return true;
}
@Input() spinner = false;
@Input() headerLogoLinkUrl?: string;
@Input() footerConfig?: FooterConfiguration;
@ContentChild('footerTemplate', { static: true, read: TemplateRef })
public footerTemplate?: TemplateRef<FooterConfiguration>;
@Input() stageConfig?: StageConfiguration;
@Input() navigationConfig?: NavigationConfig;
@Input() offerCodeStateKey?: string;
@ContentChild('headerTemplate', { static: true, read: TemplateRef })
public headerTemplate?: TemplateRef<unknown>;
hasStage$!: Observable<boolean>;
offerCode$: Observable<string | undefined> | undefined;
actionsBackgroundColor: string | undefined = 'transparent';
errorMessage: string | undefined;
hasBannerContent = false;
hasJumpNavigationMenu = false;
private domChanges!: MutationObserver;
private lastNotificationsDisplayedTotal = 0;
isRetailChannel: boolean;
@ViewChild('frameContent', { read: ElementRef }) private frameContent!: ElementRef<HTMLElement>;
@ViewChild('frameHeader', { read: ElementRef }) private frameHeader:
| ElementRef<HTMLElement>
| undefined;
@ViewChild('frameBanner', { read: ElementRef }) private frameBanner!: ElementRef<HTMLElement>;
private destroyRef = inject(DestroyRef);
private aclService = inject(AclService);
private bannerService = inject(TalyFrameBannerService);
private stickyService = inject(TalyStickyService);
@HostBinding('class.use-enhanced-vertical-spacing')
useEnhancedVerticalSpacing = inject(USE_ENHANCED_VERTICAL_SPACING);
private webComponentId = inject(WEB_COMPONENT_ID, { optional: true });
private router = inject(Router);
private ngZone = inject(NgZone);
protected hasMargin = !inject(NO_MARGIN);
protected initialRoutingDone = false;
constructor() {
const channel = inject<CHANNEL>(CHANNEL_TOKEN);
this.isRetailChannel = channel === CHANNEL.RETAIL;
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
take(1)
)
.subscribe(() => {
this.initialRoutingDone = true;
});
}
ngOnInit(): void {
this.setSubscriptions();
this.setOfferCode();
this.disableBackgroundScrolling();
this.setJumpNavigationMenuFlag();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['navigationConfig']?.firstChange === false) {
this.setJumpNavigationMenuFlag();
}
}
private setJumpNavigationMenuFlag() {
this.hasJumpNavigationMenu = Boolean(
this.navigationConfig?.jumpNavigationMenu &&
this.navigationConfig?.jumpNavigationMenu.items.length > 0
);
}
onResize(value: number) {
this.el.nativeElement.style.setProperty('--navigation-width', `${value}px`);
}
private setOfferCode() {
if (this.offerCodeStateKey) {
this.offerCode$ = this.frameNavigationService.getOfferCode$(this.offerCodeStateKey);
if (this.offerCode$) {
this.offerCode$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((offerCode) => (this.hasOfferCodeContent = Boolean(offerCode)));
}
}
}
private setSubscriptions() {
this.frameLayoutService.isStackedLayoutObservable
.pipe(
takeUntilDestroyed(this.destroyRef),
filter(() => !this.hasJumpNavigationMenu)
)
.subscribe((value) => (this.stackedLayout = value));
this.frameLayoutService.collapseNavigationUser$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => (this.collapsedNavigation = value));
this.sidebarService.setVisibility(this.sidebar);
this.sidebarService.show$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => (this.sidebar = value));
this.hasStage$ = this.pageDataService.pageData$.pipe(
takeUntilDestroyed(this.destroyRef),
map((pageData) => Object.keys(pageData.stage ?? {}).length > 0)
);
this.hasStage$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
this._hasStage = value;
});
this.bannerService.template$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((template) => (this.hasBannerContent = Boolean(template)));
this.pageDataService.pageId$
.pipe(
takeUntilDestroyed(this.destroyRef),
// in case of a web component:
// skip the first emitted page ID to avoid focusing something.
// Focusing automatically scrolls the focused element into view.
// We don't want that behavior for web components.
// See https://github.developer.allianz.io/ilt/taly-workspace/issues/2546
skip(this.webComponentId ? 1 : 0)
)
.subscribe(() => setTimeout(() => this.setFocus()));
this.smallPrintService.templateList$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((templateList) => (this.hasSmallPrintContent = templateList.length > 0));
this.pageDataService.pageId$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
this.pageId = value;
});
}
ngAfterViewInit(): void {
this.listenToDomChanges();
this.stickyService.addStickyElement(this.frameBanner);
if (!this.isRetailChannel && this.frameHeader) {
this.stickyService.addStickyElement(this.frameHeader);
}
}
disableBackgroundScrolling(): void {
if (this.spinner) {
this.frameNavigationService.applicationBusy$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((isApplicationBusy: boolean) => {
const overflowValue = isApplicationBusy ? 'hidden' : 'visible';
this.renderer.setStyle(document.body, 'overflow', overflowValue);
});
}
}
private listenToDomChanges(): void {
const hasContent = (section: HTMLElement) =>
Boolean(section.querySelector('.building-block-element'));
this.ngZone.runOutsideAngular(() => {
this.domChanges = new MutationObserver(() => {
this.manageFirstVisibleContentAreaSection();
this.scrollToNotificationsIfNecessary();
const allSections: HTMLElement[] = Array.from(
this.frameContent.nativeElement.querySelectorAll('.section-wrapper')
);
const allPanels: HTMLElement[] = Array.from(
this.frameContent.nativeElement.querySelectorAll('.panel-wrapper')
);
[...allSections, ...allPanels].forEach((item) => {
item.classList.toggle('is-empty', !hasContent(item));
});
if (this.isRetailChannel && !this.useEnhancedVerticalSpacing) {
const lastVisibleSection: HTMLElement | undefined = allSections
.reverse()
.find(hasContent);
this.actionsBackgroundColor =
lastVisibleSection?.style?.getPropertyValue('--background-color') || 'transparent';
}
});
});
this.domChanges.observe(this.frameContent.nativeElement, {
subtree: true,
childList: true
});
}
private manageFirstVisibleContentAreaSection(): void {
const hasContent = (section: HTMLElement) =>
Boolean(section.querySelector('.building-block-element'));
const allContentAreaSections: HTMLElement[] = Array.from(
this.frameContent.nativeElement.querySelectorAll('.section')
);
allContentAreaSections.forEach(function resetPreviousFirstContentAreaSection(item) {
item.classList.remove('first-with-content');
});
const firstVisibleSection: HTMLElement | undefined = allContentAreaSections.find(hasContent);
firstVisibleSection?.classList.add('first-with-content');
}
private scrollToNotificationsIfNecessary(): void {
const currentNotificationsDisplayedTotal = Array.from(
this.frameContent.nativeElement.querySelectorAll('.js-notification-content')
).length;
if (currentNotificationsDisplayedTotal > this.lastNotificationsDisplayedTotal) {
const scrollMargin = this.stickyService.getStickyElementHeights();
this.renderer.setStyle(
this.frameContent.nativeElement,
'scroll-margin-top',
`${scrollMargin}px`
);
this.frameContent.nativeElement.scrollIntoView({ behavior: 'smooth' });
}
this.lastNotificationsDisplayedTotal = currentNotificationsDisplayedTotal;
}
private setFocus() {
const hasStage = this.pageDataService.pageData?.stage?.showAsStage;
if (hasStage) {
const stage = document.querySelector<HTMLElement>('.js-stage-headline');
stage?.focus();
return;
}
const pageTitle = document.querySelector<HTMLElement>('.js-page-headline');
if (pageTitle) {
pageTitle.focus();
return;
}
if (this.hasBannerContent) {
const banner = document.querySelector<HTMLElement>('.js-banner');
banner?.focus();
return;
}
const overarchingDetails = document.querySelector<HTMLElement>('.js-overarching-details');
if (overarchingDetails) {
overarchingDetails.focus();
return;
}
document.getElementById('frameContent')?.focus();
}
ngOnDestroy(): void {
this.domChanges?.disconnect();
}
}
<div class="grid-container" [class.no-margin]="!hasMargin">
@if (!chromeless) {
<frame-header
#frameHeader
data-testid="frame-header"
data-section-name="header"
[title]="title"
[logoSrc]="logoSrc"
class="header"
[headerTemplate]="headerTemplate"
[headerLogoLinkUrl]="headerLogoLinkUrl"
>
<ng-content select="[talyFrameHeaderActions]"></ng-content>
</frame-header>
} @if (spinner && (frameNavigationService.applicationBusy$ | async)) {
<taly-spinner
class="spinner"
data-testid="taly-spinner"
[label]="frameNavigationService.applicationBusyMessage$ | async"
>
</taly-spinner>
} @if (hasStage$ | async) {
<frame-stage
class="stage"
data-testid="stage"
data-section-name="stage"
[stageConfig]="stageConfig"
[centered]="centered"
></frame-stage>
}
<frame-banner
#frameBanner
class="banner js-banner"
tabindex="-1"
data-testid="frame-banner"
data-section-name="banner"
></frame-banner>
@if (initialRoutingDone && hasNavigation) {
<div
class="navigation"
[class.has-margin]="hasMargin || (hasStage$ | async) || hasHeader || hasBanner"
role="navigation"
data-section-name="navigation"
>
<frame-navigation
[navigationConfig]="navigationConfig"
[hasJumpNavigationMenu]="hasJumpNavigationMenu"
(resizeEvent)="onResize($event)"
data-testid="frame-navigation"
></frame-navigation>
</div>
}
<frame-content
class="content"
role="main"
data-testid="frame-content"
#frameContent
id="frameContent"
tabindex="-1"
>
<ng-content talyFrameContent></ng-content>
@if (initialRoutingDone) {
<frame-actions
talyFrameActions
class="actions"
[ngStyle]="{ backgroundColor: actionsBackgroundColor }"
[hasSmallPrint]="hasSmallPrintContent"
[hasFooter]="hasFooter"
[hasOfferCode]="hasOfferCode"
data-testid="frame-actions"
data-section-name="actions"
></frame-actions>
} @if (isRetailChannel && offerCode$ && (offerCode$ | async)) {
<frame-offer-code
talyFrameOfferCode
[offerCode$]="offerCode$"
[hasSmallPrint]="hasSmallPrintContent"
[hasFooter]="hasFooter"
[class.has-margin]="hasMargin"
class="offer-code"
data-testid="offer-code"
data-section-name="offer code"
></frame-offer-code>
}
<frame-small-print
talyFrameSmallPrint
class="small-print"
data-section-name="small print"
[class.has-margin]="hasMargin"
></frame-small-print>
</frame-content>
<div class="sidebar" data-section-name="sidebar">
<frame-sidebar></frame-sidebar>
</div>
@if (!chromeless) {
<frame-footer
class="footer"
[class.no-margin]="!hasMargin"
data-section-name="footer"
[footerConfig]="footerConfig"
[footerTemplate]="footerTemplate"
></frame-footer>
}
</div>
@use '../../styles/breakpoints.scss' as *;
@use '../../styles/spacing.scss' as *;
@use '../../styles/navigation.scss' as *;
$sidebar-width: 350px;
:host {
display: block;
margin: auto;
max-width: 100%;
height: 100%;
// CSS custom properties for padding based on layout state
&.use-enhanced-vertical-spacing {
&.has-navigation,
&.has-header,
&.has-banner {
--first-section-padding-top: var(--vertical-sub-section-outer);
}
&.has-stage {
--first-section-padding-top: var(--vertical-layout-helper);
}
&.has-navigation,
&.has-stage,
&.has-header,
&.has-banner {
--overarching-details-padding-top: var(--vertical-layout-helper);
--frame-group-notification-padding: var(--vertical-layout-helper);
}
}
&:not(.use-enhanced-vertical-spacing) {
&.has-navigation,
&.has-stage,
&.has-header,
&.has-banner {
--first-section-padding-top: var(--vertical-outer-section-spacing);
--frame-group-notification-padding: var(--vertical-layout-helper);
--overarching-details-padding-top: var(--vertical-layout-helper);
}
&.has-navigation,
&.has-header,
&.has-banner {
--overarching-details-half-padding-top: var(--half-vertical-outer-sub-section-spacing);
}
&.has-stage {
--overarching-details-half-padding-top: var(--half-vertical-layout-helper);
}
}
&:not(.has-navigation):not(.has-stage):not(.has-header):not(.has-banner) {
--first-section-padding-top: 0;
--frame-group-notification-padding: 0;
--overarching-details-padding-top: 0;
--overarching-details-half-padding-top: 0;
}
}
.grid-container {
display: grid;
min-height: 100%;
.header {
grid-area: header;
}
.navigation {
grid-area: navigation;
}
.footer {
grid-area: footer;
}
.stage {
grid-area: stage;
}
.banner {
grid-area: banner;
position: sticky;
top: var(--header-height, 0);
z-index: 3;
outline: none;
}
.content {
grid-area: content;
z-index: 1;
&:focus {
outline: none;
}
}
.actions,
.small-print {
display: block;
}
.sidebar {
grid-area: sidebar;
width: $sidebar-width;
}
.spinner {
display: flex;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
position: fixed;
z-index: 10000;
}
}
/*
* Tool for initial creation: https://grid.layoutit.com/
*/
:host:not(.is-stacked) {
--navigation-width: #{$navigation-width};
// NDBX nx-header height
--header-height: 64px;
&.no-header {
--header-height: 0;
}
&:not(.has-sidebar) .sidebar {
display: none;
}
&:not(.has-navigation) .navigation {
display: none;
}
&.collapsed-navigation {
--navigation-width: #{$navigation-collapsed-width};
}
.grid-container {
grid-template-rows: min-content min-content min-content 1fr min-content;
}
.offer-code.has-margin,
.small-print.has-margin {
@include frame-spacing;
}
.header {
position: sticky;
top: 0;
z-index: 1;
}
.content,
.footer {
z-index: 0;
}
&.has-navigation {
&.has-sidebar .grid-container {
grid-template-columns: var(--navigation-width) minmax(0, 1fr) $sidebar-width;
grid-template-areas:
'header header header'
'navigation stage sidebar'
'navigation banner sidebar'
'navigation content sidebar'
'navigation footer sidebar';
}
&:not(.has-sidebar) .grid-container {
grid-template-columns: var(--navigation-width) minmax(0, 1fr);
grid-template-areas:
'header header'
'navigation stage'
'navigation banner'
'navigation content'
'navigation footer';
}
}
&:not(.has-navigation) {
&.has-sidebar .grid-container {
grid-template-columns: minmax(0, 1fr) $sidebar-width;
grid-template-areas:
'header header'
'stage sidebar'
'banner sidebar'
'content sidebar'
'footer sidebar';
}
}
&:not(.has-sidebar) .grid-container {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
'header'
'stage'
'banner'
'content'
'footer';
}
}
:host(.is-stacked) {
.grid-container {
grid-template-columns: minmax(320px, 1fr);
.offer-code.has-margin,
.small-print.has-margin {
@include frame-spacing;
}
}
&:not(.has-navigation) {
.grid-container {
grid-template-rows: min-content min-content min-content auto min-content;
grid-template-areas:
'header'
'stage'
'banner'
'content'
'footer';
}
.navigation {
display: none;
}
}
&.has-navigation .grid-container {
grid-template-rows: min-content min-content min-content min-content auto min-content;
grid-template-areas:
'header'
'stage'
'banner'
'navigation'
'content'
'footer';
.navigation.has-margin {
padding-top: 16px;
@media (min-width: $breakpoint-m) {
padding-top: 32px;
}
}
}
.sidebar {
display: none;
}
}
.no-margin {
.offer-code,
.small-print {
@include frame-spacing-no-padding;
}
}
Legend
Html element with directive