File

app/dxa/dxa-base/dxa-base.component.ts

Implements

OnInit

Metadata

selector div[dxa-base]
styleUrls ./dxa-base.component.scss
templateUrl ./dxa-base.component.html

Index

Properties
Methods
HostBindings

Constructor

constructor(activatedRoute: ActivatedRoute, titleService: Title, dataService: DataResolverService, publicationService: PublicationService, constants: ConstantsService, rowResolver: RowResolverService, util: UtilService, viewportScroller: ViewportScroller, labelService: LabelService, loadingService: LoadingIndicatorService, cdRef: ChangeDetectorRef)
Parameters :
Name Type Optional
activatedRoute ActivatedRoute No
titleService Title No
dataService DataResolverService No
publicationService PublicationService No
constants ConstantsService No
rowResolver RowResolverService No
util UtilService No
viewportScroller ViewportScroller No
labelService LabelService No
loadingService LoadingIndicatorService No
cdRef ChangeDetectorRef No

HostBindings

class.bg-light-slate

Methods

Private anchorScrollListener
anchorScrollListener()
Returns : void
enableBackToTop
enableBackToTop()
Returns : void
extractPageDetails
extractPageDetails()

Finds and removes the Page Details component from the header if it exists.

Returns : any
Public fetchPageAndNavigation
fetchPageAndNavigation()
Returns : void
goToTop
goToTop()
Returns : void
Private movePageDetailsToTop
movePageDetailsToTop()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Private pageDetailsShouldBeMoved
pageDetailsShouldBeMoved()
Returns : boolean
Private resolveDynamicComponentInRegion
resolveDynamicComponentInRegion(region: any[])

Resolve each dynamic component in the given region. Updates the @param region when the data has loaded.

Parameters :
Name Type Optional Description
region any[] No

The region

Returns : void
Private resolveDynamicComponentsInRow
resolveDynamicComponentsInRow(row: any[])

Resolve each dynamic component in the given row. Returns an updated row when all components have been resolved.

Parameters :
Name Type Optional Description
row any[] No

A row in a region.

Returns : Promise<any>
Public setUpRegions
setUpRegions()
Returns : void

Properties

Public activatedRoute
Type : ActivatedRoute
backToTopLabel
Type : string
breadcrumbOnPage
Type : boolean
Public constants
Type : ConstantsService
darkTopRegion
Type : boolean
dockToFooter
Type : number
headerRegion
Type : []
Default value : []
isLoading
Default value : false
isPdp
Default value : false
isSupportPage
Type : boolean
isSystemPage
Type : boolean
mainRegion
Type : []
Default value : []
navigation
newPageDataSubject
Default value : new Subject<boolean>()
newPageObs
Default value : this.newPageDataSubject.asObservable()
page
regions
Type : []
Default value : []
showScrollTopButton
Type : boolean
subscriptions
Type : []
Default value : []
topRegion
Type : []
Default value : []
Public util
Type : UtilService
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, HostBinding, ChangeDetectorRef } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { DataResolverService } from 'src/app/core/services/data-resolver/data-resolver.service';
import { ConstantsService } from 'src/app/shared/constants/constants.service';
import { RowResolverService } from 'src/app/core/services/row-resolver.service';
import { UtilService } from 'src/app/core/services/util-service/util.service';
import { PublicationService } from 'src/app/core/services/publication-service/publication.service';
import { ViewportScroller } from '@angular/common';
import { debounceTime } from 'rxjs/operators';
import { LabelService } from 'src/app/core/services/label-service/label-service.service';
import { Subject, fromEvent } from 'rxjs';
import { LoadingIndicatorService } from 'src/app/core/services/loading-indicator.service';

@Component({
	selector: 'div[dxa-base]',
	templateUrl: './dxa-base.component.html',
	styleUrls: ['./dxa-base.component.scss']
})
export class DxaBaseComponent implements OnInit {

	page;
	navigation;
	subscriptions = [];

	darkTopRegion: boolean;
	breadcrumbOnPage: boolean;
	isSupportPage: boolean;
	isSystemPage: boolean;
	isLoading = false;
	showScrollTopButton: boolean;
	isPdp = false;

	backToTopLabel: string;
	dockToFooter: number;

	headerRegion = [];
	topRegion = [];
	mainRegion = [];

	regions = [];

	newPageDataSubject = new Subject<boolean>();
	newPageObs = this.newPageDataSubject.asObservable();

	@HostBinding('class.bg-light-slate') get bgColor() { return this.isSupportPage; }

	constructor(
		public activatedRoute: ActivatedRoute,
		private titleService: Title,
		private dataService: DataResolverService,
		private publicationService: PublicationService,
		public constants: ConstantsService,
		private rowResolver: RowResolverService,
		public util: UtilService,
		private viewportScroller: ViewportScroller,
		private labelService: LabelService,
		private loadingService: LoadingIndicatorService,
		private cdRef: ChangeDetectorRef
	) { }

	ngOnInit() {
		this.labelService.getLabel('backToTopNav').then(label => this.backToTopLabel = label);

		// Loading state for page loading spinner.
		this.loadingService.contentLoaderState.subscribe(loadingState => {
			this.isLoading = loadingState;
			this.cdRef.detectChanges();
		});

		// Set the title to skf.com here, will be overwritten in the pagedetails entity
		this.titleService.setTitle('SKF.com');
		this.fetchPageAndNavigation();
		this.anchorScrollListener();
		this.enableBackToTop();

	}

	private anchorScrollListener() {
		// Scroll to anchor when new page data loads if anchor links exists in URL.
		this.viewportScroller.setOffset([0, 30]);
		this.newPageObs.subscribe(() => {
			const frag = this.activatedRoute.snapshot.fragment;
			if (frag) {
				// Wait for components to be rendered before scrolling.
				setTimeout(() => {
					document.getElementById(frag).scrollIntoView({behavior: 'smooth'});
				}, 1000);
			} else {
				// If no anchor tag, scroll to top of page.
				this.viewportScroller.scrollToPosition([0, 0]);
			}
		});
	}

	enableBackToTop(): void {
		fromEvent(document, 'scroll').pipe(debounceTime(100)).subscribe(() => {
			this.showScrollTopButton = window.pageYOffset > 200;

			const footerObserver = new IntersectionObserver((entries) => {
				entries.forEach(entry => {
					if (entry.isIntersecting) {
						const rect = entry.target.getBoundingClientRect();
						const intersect: number = entry.intersectionRatio === 1 ? rect.height : (entry.intersectionRatio * rect.height);
						this.dockToFooter = intersect;
					} else {
						this.dockToFooter = 0;
					}
				}, { rootMargin: '0px', threshold: 1.0 });
			});

			const sole = document.querySelector('.footer-sole-region');
			if (sole) {
				footerObserver.observe(sole);
			}
		});
	}

	public fetchPageAndNavigation(): void {
		// Fetch page and navigation data when URL changes
		this.subscriptions.push(this.activatedRoute.url.subscribe(res => {
			this.isPdp = this.activatedRoute.snapshot.data.isPDP;	
			
			if (!this.isPdp) {	// Product Detail Pages are not Tridion pages, so we don't try to fetch the page from Tridion.
				const url = '/' + res.join('/');
				this.loadingService.startLoading();

				this.dataService.getPageData(url).then(page => {
					this.loadingService.loadingDone();
					this.page = page;
					this.setUpRegions();
					this.newPageDataSubject.next(true);
				}, () => {
					this.loadingService.loadingDone();
				});
			}

			this.dataService.getNavigationData().then(nav => this.navigation = nav);
		}));
	}

	public setUpRegions() {
		this.darkTopRegion = false;
		this.breadcrumbOnPage = true;

		this.isSupportPage = this.util.extract(this.page, 'MvcData', 'ViewName') === 'supportPage';

		// Do not show breadcrumbs on system pages
		const re = RegExp(this.publicationService.getPublicationPath() + '/system');
		this.isSystemPage = re.test(this.util.extract(this.page, 'Url'));

		const regions = this.page.Regions;

		const headReg = regions.find(region => region.Name === 'header');
		this.headerRegion = headReg ? headReg.Entities : [];

		const topReg = regions.find(region => region.Name === 'top');
		this.topRegion = topReg ? topReg.Entities : [];

		const mainReg = regions.find(region => region.Name === 'main');
		this.mainRegion = mainReg ? mainReg.Entities : [];

		// Business requirement: If PageDetails is the last or only entity in header region, move it to:
		// * TopBannerTop if it's the first entity in the top region.
		// * TopBannerMain if top region is empty and it's the first entity in main region.
		const topBannerTop = this.util.extract(this.topRegion, [0]);	// Extract first entity in topRegion
		const topBannerMain = this.util.extract(this.mainRegion, [0]);	// Extract first entity in mainRegion

		let pageDetailsData;
		let topBanner;
		const topViewName = this.util.extract(topBannerTop, 'MvcData', 'ViewName');

		if (topViewName === 'TopBannerTop' || topViewName === 'TopBannerWSearchTop' || topViewName === 'TopBannerWSearchLargeTop') {
			topBanner = topBannerTop;
		} else if (this.util.extract(topBannerMain, 'MvcData', 'ViewName') === 'TopBannerMain' && this.topRegion.length === 0) {
			topBanner = topBannerMain;
		}

		if (topBanner) {
			pageDetailsData = this.extractPageDetails();
			if (pageDetailsData) {
				topBanner.PageDetailsData = pageDetailsData;
				topBanner.includeBreadcrumb = true;
			}
			this.breadcrumbOnPage = false;
		}

		// Code above might move PageDetails if rules apply. That's expected.
		if (this.page.MvcData.ViewName === 'contentWTopArea' && this.pageDetailsShouldBeMoved()) {
			this.movePageDetailsToTop();
		}
		this.headerRegion = this.rowResolver.transform(this.headerRegion, 'HEADER');
		this.mainRegion = this.rowResolver.transform(this.mainRegion, 'MAIN');
		this.topRegion = this.rowResolver.transform(this.topRegion, 'TOP');
		// Currently only 'MAIN' has dynamic components.
		this.resolveDynamicComponentInRegion(this.mainRegion);
	}

	/*
	These are used to fulfil the business requirement that if the a component using the BodyText [top] component template
	is present on the page and if the header region has a component using the PageDetails [header] component template then the component
	using the PageDetails [header] component template should be removed from the header region amd added to the top region
	*/
	private pageDetailsShouldBeMoved(): boolean {
		let isPageDetailPresent = false;
		let isBodyTextPresent = false;

		isBodyTextPresent = this.topRegion.find(entity => entity.MvcData.ViewName === 'BodyText');
		isPageDetailPresent = this.headerRegion.find(entity => entity.MvcData.ViewName === 'PageDetails') 
			|| this.headerRegion.find(entity => entity.MvcData.ViewName === 'PageDetailsHorizontal');

		return isPageDetailPresent && isBodyTextPresent;
	}

	/** Finds and removes the Page Details component from the header if it exists.  */
	extractPageDetails() {
		const headerRegion = this.page.Regions.find(region => region.Name === 'header');

		if (headerRegion && headerRegion.Entities.length > 0) {
			const pageDetails = this.util.extract(headerRegion, 'Entities', [headerRegion.Entities.length - 1], 'MvcData', 'ViewName');

			if (pageDetails === 'PageDetails' || pageDetails === 'PageDetailsHorizontal') {
				const pdComponent = headerRegion.Entities.pop();
				return pdComponent;
			}
		}
	}

	private movePageDetailsToTop(): void {
		// delete pageDetails from header region
		const pageDetails = this.util.pickOneFromArray(this.headerRegion, entitiy => entitiy.MvcData.ViewName === 'PageDetails') 
			|| this.util.pickOneFromArray(this.headerRegion, entitiy => entitiy.MvcData.ViewName === 'PageDetailsHorizontal');
		// Add it as the first element in the top region
		this.topRegion.unshift(pageDetails);
		this.darkTopRegion = true;
	}

	/**
	 * Resolve each dynamic component in the given row. Returns an updated row when all
	 * components have been resolved. 
	 * @param row A row in a region.
	 */
	private resolveDynamicComponentsInRow(row: any[]): Promise<any> {
		let finishedEntity = 0;
		const newRow = new Array(row.length);

		return new Promise<any>((resolve, reject) => {
			row.forEach((entity, index) => {
	
				// If the Ids is of the format <number>-<number> then we must resolve it manually. Since it's a dynamic component.
				const id = this.util.extract(entity, 'Id') || '';
				if (id.includes('-')) {
	
					const arrayIds = id.split('-');
					const componentId = arrayIds[0];
					const templateId = arrayIds[1];
	
					this.dataService.getDynamicComponent(componentId, templateId).then((resolvedContent) => {
						newRow[index] = resolvedContent;
						finishedEntity += 1;
						// Return updated row when all components have been resolved.
						if (finishedEntity === row.length) {
							resolve(newRow);
						}
	
					}).catch(error => {
						// Keep track even if component gives error.
						finishedEntity += 1;
						if (finishedEntity === row.length) { resolve(newRow); }
						console.log(error);
					});
				} else {
					newRow[index] = entity;
					finishedEntity += 1;
					// Return updated row when all components have been resolved.
					if (finishedEntity === row.length) { resolve(newRow); }
				}
			});
		});
	}

	/**
	 * Resolve each dynamic component in the given region. Updates the @param region when the data has loaded.
	 * @param region The region
	 */
	private resolveDynamicComponentInRegion( region: any[]): void {
		region.forEach((row, index) => {
			const entity = this.util.extract(row, [0]);	// Extract first entity in row
			const entityViewName = this.util.extract(entity, 'MvcData', 'ViewName');
			// Resolve tab content dynamic components.
			if (entityViewName === 'TabMain') {
				const entityCopy = Object.assign({}, entity);	// Make copy to make sure that re-render occurs.
				const innerEntities = entityCopy ? entityCopy.Entities : [];

				innerEntities.forEach((innerEntity, i) => {
					const tabContnetEntities = innerEntity ? innerEntity.TabContnetEntities : [];
					this.resolveDynamicComponentsInRow(tabContnetEntities).then(updated => {
						innerEntities[i].TabContnetEntities = updated;
						region[index] = [entityCopy];
					});
				});
			} else {
				this.resolveDynamicComponentsInRow(row).then(updated => region[index] = updated);
			}
		});
	}

	goToTop() {
		window.scroll(
			{
				'behavior': 'smooth',
				'left': 0,
				'top': 0
			}
		);
	}
}
<div cookies-banner></div>
<nav auth-bar></nav>
<nav top-menu role="navigation" [page]="page" [navigation]="navigation" *ngIf="navigation && !navigation.error"></nav>
<div *ngIf="navigation && navigation.error">NAVIGATION ERROR</div>

<div class="loading" *ngIf="isLoading">
    <div class="lds-dual-ring">
        <img class="logo" src="v2/assets/img/skf-logo-blue.svg">
    </div>
</div>

<button class="btn scroll-top" (click)="goToTop()" [ngClass]="{'show-scrollTop': showScrollTopButton}" [ngStyle]="{'bottom.px': dockToFooter}">
    <i class="icon-chevronUp"></i> {{ backToTopLabel }}
</button>

<ng-container *ngIf="page && page.error && !isLoading">
    <div class="page-not-found">
        <div class="container">
            <h1 class="notfound-header">Error</h1>
            <h2>Uh-oh, something went wrong.</h2>

            <div class="searchbox">
                <search-input></search-input>
            </div>
        </div>
    </div>
</ng-container>

<div [hidden]="isLoading">
    <div breadcrumb [ngClass]="{'bg-cool-grey': darkTopRegion}" [themeColor]="darkTopRegion ? 'light' : 'dark'" *ngIf="(breadcrumbOnPage && !isSystemPage && !isLoading) || isPdp"></div>
    
    <ng-container *ngIf="!isPdp">
        <header id="header" role="banner" *ngIf="headerRegion.length > 0">
            <section dxa-entity [entities]="row" [region]="'HEADER'" *ngFor="let row of headerRegion"></section>
        </header>
    
        <header id="top" role="banner" *ngIf="topRegion.length > 0"
            [ngClass]="{'bg-cool-grey top-region-body-text': darkTopRegion}">
            <section dxa-entity [entities]="row" [region]="'TOP'" *ngFor="let row of topRegion"></section>
        </header>
    
        <main id="main" role="main" *ngIf="mainRegion.length > 0">
            <section dxa-entity [entities]="row" [region]="'MAIN'" *ngFor="let row of mainRegion"></section>
        </main>
    </ng-container> 
    
    <div product-detail-page *ngIf="isPdp"></div>
</div>

<footer footer role="contentinfo"></footer>

./dxa-base.component.scss

@import "src/app/styles/helpers";

@keyframes fadein {
    0%   { opacity: 0; }
    25%  { opacity: 0; }
    100% { opacity: 1; }
}
@keyframes lds-dual-ring {
    0%   { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

:host {
    display: flex;
    flex-direction: column;
    height: 100%;
    position: relative;
    overflow-x: hidden;
}

.loading {
    animation: fadein 1.25s;

    width: 100%;
    height: 100vh;

    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    > .lds-dual-ring {
        position: relative;
        $size: 300px;
        $color: #0f58d6;

        display: flex;
        align-items: center;
        justify-content: center;

        width: $size;
        height: $size;

        &:after {
            content: " ";
            display: block;
            width: $size;
            height: $size;
            margin: 1px;
            border-radius: 50%;
            border: 2px solid #fff;
            border-color: $color $color transparent transparent;
            animation: lds-dual-ring 0.7s linear infinite;
        }

        > .logo {
            position: absolute;
            top: 45%;
            left: 25%;
            width: 150px;
        }

    }
}

.scroll-top {
    position: fixed;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    bottom: 0;
    right: 0;
    background-color: $blue;
    color: white;
    opacity: 0;
    transition: opacity .25s ease-in-out, bottom .25s ease-in;
    z-index: 1;
    padding: calc-rem(12);
    font-family: get-font-family(skf-medium);
    @include font-size(10);
    text-transform: uppercase;
    line-height: 1.2;
    letter-spacing: calc-rem(1.4);
    width: calc-rem(68);
    border: none;
    outline: none;
    box-shadow: none;

    @include media-breakpoint-up(md) {
        @include font-scale(14, demibold);
        line-height: 1.43;
        letter-spacing: calc-rem(2);
        padding: calc-rem(15);
        width: calc-rem(96);

        i {
            @include font-size(14);
        }
    }

    &:hover {
        background-color: $dark-blue;
    }

    &.show-scrollTop {
        opacity: 1;
    }

    i {
        @include font-size(12);
        margin: 0;
        margin-bottom: calc-rem(4);
        width: auto;
    }
}

.page-not-found {

    background-image:
        linear-gradient( rgba(0,0,0,.7), rgba(0,0,0,.7) ),
        url('https://images.unsplash.com/photo-1464490997959-0c65eee1cc26?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=a94e63e6f9e5de6163c5343adfe75689&auto=format&fit=crop&w=1050&q=80');
    background-size: cover;
    background-position: center;

    color: white;
    h1, h2, h3, h4, h5 { color: white; }

    padding: 150px 0;
    text-align: center;

    .notfound-header {
        font-size: 120px;
    }

    .searchbox {
        margin-top: 50px;
        max-width: 950px;
        margin-left: auto;
        margin-right: auto;
    }

}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""