From c8261870248e21cdf1da7017c1d71a3d00484ec7 Mon Sep 17 00:00:00 2001 From: Sergey Andrievskiy Date: Tue, 20 Feb 2018 14:38:03 +0300 Subject: [PATCH] feat(ssr): add server-side rendering support --- src/framework/auth/components/auth.component.ts | 6 +++++- .../theme/components/layout/layout.component.ts | 15 ++++++++++----- .../theme/components/menu/menu.component.ts | 15 +++++++++++++-- .../components/popover/popover.directive.ts | 17 +++++++++++++---- .../theme/components/search/search.component.ts | 10 +++++++--- .../components/sidebar/sidebar.component.ts | 10 +++++++--- src/framework/theme/services/spinner.service.ts | 8 +++++--- 7 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/framework/auth/components/auth.component.ts b/src/framework/auth/components/auth.component.ts index e4a4246d06..c1f12e158f 100644 --- a/src/framework/auth/components/auth.component.ts +++ b/src/framework/auth/components/auth.component.ts @@ -5,6 +5,7 @@ */ import { Component, OnDestroy } from '@angular/core'; import { NbAuthService } from '../services/auth.service'; +import { takeWhile } from 'rxjs/operators/takeWhile'; @Component({ selector: 'nb-auth', @@ -25,6 +26,8 @@ import { NbAuthService } from '../services/auth.service'; }) export class NbAuthComponent implements OnDestroy { + private alive = true; + subscription: any; authenticated: boolean = false; @@ -34,12 +37,13 @@ export class NbAuthComponent implements OnDestroy { constructor(protected auth: NbAuthService) { this.subscription = auth.onAuthenticationChange() + .pipe(takeWhile(() => this.alive)) .subscribe((authenticated: boolean) => { this.authenticated = authenticated; }); } ngOnDestroy(): void { - this.subscription.unsubscribe(); + this.alive = false; } } diff --git a/src/framework/theme/components/layout/layout.component.ts b/src/framework/theme/components/layout/layout.component.ts index d7ab68a05b..9ea23547f2 100644 --- a/src/framework/theme/components/layout/layout.component.ts +++ b/src/framework/theme/components/layout/layout.component.ts @@ -6,8 +6,9 @@ import { AfterViewInit, Component, ComponentFactoryResolver, ElementRef, HostBinding, HostListener, Input, OnDestroy, - Renderer2, ViewChild, ViewContainerRef, OnInit, + Renderer2, ViewChild, ViewContainerRef, OnInit, Inject, PLATFORM_ID, } from '@angular/core'; +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { Router, NavigationEnd } from '@angular/router'; import { Subject } from 'rxjs/Subject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; @@ -255,7 +256,7 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy { @Input() set windowMode(val: boolean) { this.windowModeValue = convertToBoolProperty(val); - this.withScroll = true; + this.withScroll = this.windowModeValue; } /** @@ -269,7 +270,7 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy { // TODO: is this the best way of doing it? as we don't have access to body from theme styles // TODO: add e2e test - const body = document.getElementsByTagName('body')[0]; + const body = this.document.getElementsByTagName('body')[0]; if (this.withScrollValue) { this.renderer.setStyle(body, 'overflow', 'hidden'); } else { @@ -291,6 +292,8 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy { protected elementRef: ElementRef, protected renderer: Renderer2, protected router: Router, + @Inject(PLATFORM_ID) protected platformId: Object, + @Inject(DOCUMENT) protected document: any, ) { this.themeService.onThemeChange() @@ -298,7 +301,7 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy { takeWhile(() => this.alive), ) .subscribe((theme: any) => { - const body = document.getElementsByTagName('body')[0]; + const body = this.document.getElementsByTagName('body')[0]; if (theme.previous) { this.renderer.removeClass(body, `nb-theme-${theme.previous}`); } @@ -331,7 +334,9 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy { this.spinnerService.load(); // trigger first time so that after the change we have the initial value - this.themeService.changeWindowWidth(window.innerWidth); + if (isPlatformBrowser(this.platformId)) { + this.themeService.changeWindowWidth(window.innerWidth); + } } ngAfterViewInit() { diff --git a/src/framework/theme/components/menu/menu.component.ts b/src/framework/theme/components/menu/menu.component.ts index 6223045e39..1892d22e58 100644 --- a/src/framework/theme/components/menu/menu.component.ts +++ b/src/framework/theme/components/menu/menu.component.ts @@ -16,7 +16,10 @@ import { QueryList, ElementRef, AfterViewInit, + PLATFORM_ID, + Inject, } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; import { Router, NavigationEnd } from '@angular/router'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { takeWhile } from 'rxjs/operators/takeWhile'; @@ -49,7 +52,10 @@ export class NbMenuItemComponent implements AfterViewInit, OnDestroy { @ViewChildren(NbMenuItemComponent, { read: ElementRef }) subMenu: QueryList; maxHeight: number = 0; - constructor(private menuService: NbMenuService) { } + constructor( + private menuService: NbMenuService, + @Inject(PLATFORM_ID) private platformId: Object, + ) { } ngAfterViewInit() { this.subMenu.changes @@ -74,7 +80,12 @@ export class NbMenuItemComponent implements AfterViewInit, OnDestroy { } updateSubmenuHeight() { - this.menuItem.subMenuHeight = this.subMenu.reduce((acc, c) => acc + getElementHeight(c.nativeElement), 0); + if (isPlatformBrowser(this.platformId)) { + this.menuItem.subMenuHeight = this.subMenu.reduce( + (acc, c) => acc + getElementHeight(c.nativeElement), + 0, + ); + } } updateMaxHeight() { diff --git a/src/framework/theme/components/popover/popover.directive.ts b/src/framework/theme/components/popover/popover.directive.ts index f018625bd4..9b18c10f5c 100644 --- a/src/framework/theme/components/popover/popover.directive.ts +++ b/src/framework/theme/components/popover/popover.directive.ts @@ -4,7 +4,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { ComponentRef, Directive, ElementRef, HostListener, Input, OnDestroy, OnInit } from '@angular/core'; +import { + ComponentRef, Directive, ElementRef, HostListener, + Input, OnDestroy, OnInit, PLATFORM_ID, Inject, +} from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; import { NbPositioningHelper } from './helpers/positioning.helper'; import { NbPopoverComponent, NbPopoverContent } from './popover.component'; import { NbThemeService } from '../../services/theme.service'; @@ -130,11 +134,16 @@ export class NbPopoverDirective implements OnInit, OnDestroy { return this.hostRef.nativeElement; } - constructor(private hostRef: ElementRef, private themeService: NbThemeService) { - } + constructor( + private hostRef: ElementRef, + private themeService: NbThemeService, + @Inject(PLATFORM_ID) private platformId, + ) {} ngOnInit() { - this.registerTriggers(); + if (isPlatformBrowser(this.platformId)) { + this.registerTriggers(); + } } ngOnDestroy() { diff --git a/src/framework/theme/components/search/search.component.ts b/src/framework/theme/components/search/search.component.ts index 5ce3d57fe1..8871b9ec64 100644 --- a/src/framework/theme/components/search/search.component.ts +++ b/src/framework/theme/components/search/search.component.ts @@ -30,6 +30,7 @@ import { delay } from 'rxjs/operators/delay'; import { NbSearchService } from './search.service'; import { NbThemeService } from '../../services/theme.service'; +import { takeWhile } from 'rxjs/operators/takeWhile'; /** * search-field-component is used under the hood by nb-search component @@ -169,6 +170,8 @@ export class NbSearchFieldComponent { }) export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { + private alive = true; + /** * Tags a search with some ID, can be later used in the search service * to determine which search component triggered the action, if multiple searches exist on the page. @@ -218,6 +221,7 @@ export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit() { this.routerSubscription = this.router.events .pipe( + takeWhile(() => this.alive), filter(event => event instanceof NavigationEnd), ) .subscribe(event => this.searchService.deactivateSearch(this.searchType, this.tag)); @@ -227,6 +231,7 @@ export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { this.searchService.onSearchActivate(), ]) .pipe( + takeWhile(() => this.alive), filter(([componentRef, data]: [ComponentRef, any]) => !this.tag || data.tag === this.tag), ) .subscribe(([componentRef]: [ComponentRef]) => { @@ -246,6 +251,7 @@ export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { this.searchService.onSearchDeactivate(), ]) .pipe( + takeWhile(() => this.alive), filter(([componentRef, data]: [ComponentRef, any]) => !this.tag || data.tag === this.tag), ) .subscribe(([componentRef]: [ComponentRef]) => { @@ -294,9 +300,7 @@ export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { } ngOnDestroy() { - this.activateSearchSubscription.unsubscribe(); - this.deactivateSearchSubscription.unsubscribe(); - this.routerSubscription.unsubscribe(); + this.alive = false; const componentRef = this.searchFieldComponentRef$.getValue(); if (componentRef) { diff --git a/src/framework/theme/components/sidebar/sidebar.component.ts b/src/framework/theme/components/sidebar/sidebar.component.ts index 06073643a1..98a56c66ba 100644 --- a/src/framework/theme/components/sidebar/sidebar.component.ts +++ b/src/framework/theme/components/sidebar/sidebar.component.ts @@ -6,6 +6,7 @@ import { Component, HostBinding, Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; +import { takeWhile } from 'rxjs/operators/takeWhile'; import { convertToBoolProperty } from '../helpers'; import { NbThemeService } from '../../services/theme.service'; @@ -117,6 +118,8 @@ export class NbSidebarComponent implements OnInit, OnDestroy { protected stateValue: string; protected responsiveValue: boolean = false; + private alive = true; + @HostBinding('class.fixed') fixedValue: boolean = false; @HostBinding('class.right') rightValue: boolean = false; @HostBinding('class.left') leftValue: boolean = true; @@ -212,6 +215,7 @@ export class NbSidebarComponent implements OnInit, OnDestroy { ngOnInit() { this.toggleSubscription = this.sidebarService.onToggle() + .pipe(takeWhile(() => this.alive)) .subscribe((data: { compact: boolean, tag: string }) => { if (!this.tag || this.tag === data.tag) { this.toggle(data.compact); @@ -219,6 +223,7 @@ export class NbSidebarComponent implements OnInit, OnDestroy { }); this.expandSubscription = this.sidebarService.onExpand() + .pipe(takeWhile(() => this.alive)) .subscribe((data: { tag: string }) => { if (!this.tag || this.tag === data.tag) { this.expand(); @@ -226,6 +231,7 @@ export class NbSidebarComponent implements OnInit, OnDestroy { }); this.collapseSubscription = this.sidebarService.onCollapse() + .pipe(takeWhile(() => this.alive)) .subscribe((data: { tag: string }) => { if (!this.tag || this.tag === data.tag) { this.collapse(); @@ -234,9 +240,7 @@ export class NbSidebarComponent implements OnInit, OnDestroy { } ngOnDestroy() { - this.toggleSubscription.unsubscribe(); - this.expandSubscription.unsubscribe(); - this.collapseSubscription.unsubscribe(); + this.alive = false; if (this.mediaQuerySubscription) { this.mediaQuerySubscription.unsubscribe(); } diff --git a/src/framework/theme/services/spinner.service.ts b/src/framework/theme/services/spinner.service.ts index 01bac69697..61e6ab788a 100644 --- a/src/framework/theme/services/spinner.service.ts +++ b/src/framework/theme/services/spinner.service.ts @@ -4,17 +4,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { Injectable } from '@angular/core'; +import { Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; /** * Service to control the global page spinner. */ -@Injectable() export class NbSpinnerService { private loaders: Promise[] = []; private selector: string = 'nb-global-spinner'; + constructor(@Inject(DOCUMENT) private document) {} + /** * Appends new loader to the list of loader to be completed before * spinner will be hidden @@ -66,6 +68,6 @@ export class NbSpinnerService { } private getSpinnerElement() { - return document.getElementById(this.selector); + return this.document.getElementById(this.selector); } }