From 25aab6b19ca4983ba2a3ee93032089decc0594c5 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Thu, 18 Apr 2019 14:07:36 +0800 Subject: [PATCH] feat: support server-side rendering close #3222, close #43 ref #2025,#2474 style: fix lint docs: add the docs docs: fix ssr docs docs: update README chore: rebase --- README-zh_CN.md | 1 + README.md | 1 + components/affix/nz-affix.component.ts | 50 +++++--- components/anchor/nz-anchor-link.component.ts | 6 +- components/anchor/nz-anchor.component.ts | 13 +- components/avatar/nz-avatar.component.ts | 12 +- components/back-top/nz-back-top.component.ts | 11 +- components/carousel/nz-carousel.component.ts | 7 +- components/carousel/nz-carousel.spec.ts | 87 +++++++------ components/icon/nz-icon.directive.ts | 24 ++-- components/menu/nz-submenu.component.ts | 4 +- components/select/nz-select.component.ts | 6 +- components/slider/nz-slider.component.ts | 7 +- .../statistic/nz-countdown.component.ts | 17 +-- components/table/nz-table.component.ts | 5 + components/timeline/nz-timeline.component.ts | 9 +- components/upload/nz-upload-list.component.ts | 7 +- docs/animations.en-US.md | 2 +- docs/animations.zh-CN.md | 2 +- docs/changelog.en-US.md | 2 +- docs/changelog.zh-CN.md | 2 +- docs/contributing.en-US.md | 2 +- docs/contributing.zh-CN.md | 2 +- docs/customize-theme.en-US.md | 2 +- docs/customize-theme.zh-CN.md | 2 +- docs/faq.en-US.md | 2 +- docs/faq.zh-CN.md | 2 +- docs/recommendation.en-US.md | 2 +- docs/recommendation.zh-CN.md | 2 +- docs/universal.en-US.md | 115 ++++++++++++++++++ docs/universal.zh-CN.md | 115 ++++++++++++++++++ 31 files changed, 423 insertions(+), 98 deletions(-) create mode 100644 docs/universal.en-US.md create mode 100644 docs/universal.zh-CN.md diff --git a/README-zh_CN.md b/README-zh_CN.md index 82d21e7e27c..3c97af02b2d 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -37,6 +37,7 @@ Ant Design 的 Angular 实现,开发和服务于企业级后台产品。 ## 🖥 支持环境 - Angular `^7.0.0` +- 支持服务端渲染。 - 现代浏览器,以及 Internet Explorer 11+ (使用 [polyfills](https://angular.io/guide/browser-support)) - [Electron](http://electron.atom.io/) diff --git a/README.md b/README.md index 4a799946b58..f239db86efc 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ English | [简体中文](README-zh_CN.md) ## 🖥 Environment Support * Angular `^7.0.0` +* Server-side Rendering * Modern browsers and Internet Explorer 11+ (with [polyfills](https://angular.io/guide/browser-support)) * [Electron](http://electron.atom.io/) diff --git a/components/affix/nz-affix.component.ts b/components/affix/nz-affix.component.ts index c10f565d5a9..d66d62683e6 100644 --- a/components/affix/nz-affix.component.ts +++ b/components/affix/nz-affix.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, @@ -37,10 +38,12 @@ import { export class NzAffixComponent implements OnInit, OnDestroy { @Input() set nzTarget(value: string | Element | Window) { - this.clearEventListeners(); - this._target = typeof value === 'string' ? this.doc.querySelector(value) : value || window; - this.setTargetEventListeners(); - this.updatePosition({} as Event); + if (this.platform.isBrowser) { + this.clearEventListeners(); + this._target = typeof value === 'string' ? this.doc.querySelector(value) : value || window; + this.setTargetEventListeners(); + this.updatePosition({} as Event); + } } @Input() @@ -75,13 +78,21 @@ export class NzAffixComponent implements OnInit, OnDestroy { private affixStyle: NGStyleInterface | undefined; private placeholderStyle: NGStyleInterface | undefined; - private _target: Element | Window = window; + private _target: Element | Window | null = null; private _offsetTop: number | null; private _offsetBottom: number | null; // tslint:disable-next-line:no-any - constructor(_el: ElementRef, private scrollSrv: NzScrollService, @Inject(DOCUMENT) private doc: any) { + constructor( + _el: ElementRef, + private scrollSrv: NzScrollService, + @Inject(DOCUMENT) private doc: any, + private platform: Platform + ) { this.placeholderNode = _el.nativeElement; + if (this.platform.isBrowser) { + this._target = window; + } } ngOnInit(): void { @@ -127,15 +138,19 @@ export class NzAffixComponent implements OnInit, OnDestroy { private setTargetEventListeners(): void { this.clearEventListeners(); - this.events.forEach((eventName: string) => { - this._target.addEventListener(eventName, this.updatePosition, false); - }); + if (this.platform.isBrowser) { + this.events.forEach((eventName: string) => { + this._target!.addEventListener(eventName, this.updatePosition, false); + }); + } } private clearEventListeners(): void { - this.events.forEach(eventName => { - this._target.removeEventListener(eventName, this.updatePosition, false); - }); + if (this.platform.isBrowser) { + this.events.forEach(eventName => { + this._target!.removeEventListener(eventName, this.updatePosition, false); + }); + } } private getTargetRect(target: Element | Window | undefined): ClientRect { @@ -207,11 +222,14 @@ export class NzAffixComponent implements OnInit, OnDestroy { @throttleByAnimationFrameDecorator() updatePosition(e: Event): void { - const targetNode = this._target; + if (!this.platform.isBrowser) { + return; + } + const targetNode = this._target as (HTMLElement | Window); // Backwards support let offsetTop = this.nzOffsetTop; - const scrollTop = this.scrollSrv.getScroll(targetNode, true); - const elemOffset = this.getOffset(this.placeholderNode, targetNode); + const scrollTop = this.scrollSrv.getScroll(targetNode!, true); + const elemOffset = this.getOffset(this.placeholderNode, targetNode!); const fixedNode = this.fixedEl.nativeElement as HTMLElement; const elemSize = { width: fixedNode.offsetWidth, @@ -229,7 +247,7 @@ export class NzAffixComponent implements OnInit, OnDestroy { offsetMode.top = typeof offsetTop === 'number'; offsetMode.bottom = typeof this._offsetBottom === 'number'; } - const targetRect = this.getTargetRect(targetNode); + const targetRect = this.getTargetRect(targetNode as Window); const targetInnerHeight = (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; if (scrollTop >= elemOffset.top - (offsetTop as number) && offsetMode.top) { const width = elemOffset.width; diff --git a/components/anchor/nz-anchor-link.component.ts b/components/anchor/nz-anchor-link.component.ts index b9716da9a58..094bfdd37cd 100644 --- a/components/anchor/nz-anchor-link.component.ts +++ b/components/anchor/nz-anchor-link.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -55,6 +56,7 @@ export class NzAnchorLinkComponent implements OnInit, OnDestroy { public elementRef: ElementRef, private anchorComp: NzAnchorComponent, private cdr: ChangeDetectorRef, + private platform: Platform, renderer: Renderer2 ) { renderer.addClass(elementRef.nativeElement, 'ant-anchor-link'); @@ -67,7 +69,9 @@ export class NzAnchorLinkComponent implements OnInit, OnDestroy { goToClick(e: Event): void { e.preventDefault(); e.stopPropagation(); - this.anchorComp.handleScrollTo(this); + if (this.platform.isBrowser) { + this.anchorComp.handleScrollTo(this); + } } markForCheck(): void { diff --git a/components/anchor/nz-anchor.component.ts b/components/anchor/nz-anchor.component.ts index 6e6a3c74a68..761156c6530 100644 --- a/components/anchor/nz-anchor.component.ts +++ b/components/anchor/nz-anchor.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { DOCUMENT } from '@angular/common'; import { AfterViewInit, @@ -74,8 +75,13 @@ export class NzAnchorComponent implements OnDestroy, AfterViewInit { private scroll$: Subscription | null = null; private destroyed = false; - /* tslint:disable-next-line:no-any */ - constructor(private scrollSrv: NzScrollService, @Inject(DOCUMENT) private doc: any, private cdr: ChangeDetectorRef) {} + constructor( + private scrollSrv: NzScrollService, + /* tslint:disable-next-line:no-any */ + @Inject(DOCUMENT) private doc: any, + private cdr: ChangeDetectorRef, + private platform: Platform + ) {} registerLink(link: NzAnchorLinkComponent): void { this.links.push(link); @@ -99,6 +105,9 @@ export class NzAnchorComponent implements OnDestroy, AfterViewInit { } private registerScrollEvent(): void { + if (!this.platform.isBrowser) { + return; + } this.removeListen(); this.scroll$ = fromEvent(this.getTarget(), 'scroll') .pipe( diff --git a/components/avatar/nz-avatar.component.ts b/components/avatar/nz-avatar.component.ts index 2cb895779d2..69db1755cb7 100644 --- a/components/avatar/nz-avatar.component.ts +++ b/components/avatar/nz-avatar.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -52,7 +53,8 @@ export class NzAvatarComponent implements OnChanges { private elementRef: ElementRef, private cd: ChangeDetectorRef, private updateHostClassService: NzUpdateHostClassService, - private renderer: Renderer2 + private renderer: Renderer2, + private platform: Platform ) {} setClass(): this { @@ -114,9 +116,11 @@ export class NzAvatarComponent implements OnChanges { private notifyCalc(): this { // If use ngAfterViewChecked, always demands more computations, so...... - setTimeout(() => { - this.calcStringSize(); - }); + if (this.platform.isBrowser) { + setTimeout(() => { + this.calcStringSize(); + }); + } return this; } diff --git a/components/back-top/nz-back-top.component.ts b/components/back-top/nz-back-top.component.ts index 33101e18f5e..68965b6d427 100644 --- a/components/back-top/nz-back-top.component.ts +++ b/components/back-top/nz-back-top.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, @@ -54,7 +55,12 @@ export class NzBackTopComponent implements OnInit, OnDestroy { @Output() readonly nzClick: EventEmitter = new EventEmitter(); // tslint:disable-next-line:no-any - constructor(private scrollSrv: NzScrollService, @Inject(DOCUMENT) private doc: any, private cd: ChangeDetectorRef) {} + constructor( + private scrollSrv: NzScrollService, + @Inject(DOCUMENT) private doc: any, + private platform: Platform, + private cd: ChangeDetectorRef + ) {} ngOnInit(): void { if (!this.scroll$) { @@ -86,6 +92,9 @@ export class NzBackTopComponent implements OnInit, OnDestroy { } private registerScrollEvent(): void { + if (!this.platform.isBrowser) { + return; + } this.removeListen(); this.handleScroll(); this.scroll$ = fromEvent(this.getTarget(), 'scroll') diff --git a/components/carousel/nz-carousel.component.ts b/components/carousel/nz-carousel.component.ts index 26538dc0abf..8c7d5a2dce5 100755 --- a/components/carousel/nz-carousel.component.ts +++ b/components/carousel/nz-carousel.component.ts @@ -115,6 +115,9 @@ export class NzCarouselComponent implements AfterContentInit, AfterViewInit, OnD } ngAfterViewInit(): void { + if (!this.platform.isBrowser) { + return; + } this.slickListEl = this.slickList.nativeElement; this.slickTrackEl = this.slickTrack.nativeElement; @@ -156,7 +159,9 @@ export class NzCarouselComponent implements AfterContentInit, AfterViewInit, OnD ngOnDestroy(): void { this.clearScheduledTransition(); - this.strategy.dispose(); + if (this.strategy) { + this.strategy.dispose(); + } this.dispose(); this.destroy$.next(); diff --git a/components/carousel/nz-carousel.spec.ts b/components/carousel/nz-carousel.spec.ts index 88503f63b64..1372aa597bc 100644 --- a/components/carousel/nz-carousel.spec.ts +++ b/components/carousel/nz-carousel.spec.ts @@ -12,8 +12,8 @@ import { NzCarouselModule } from './nz-carousel.module'; describe('carousel', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports : [ NzCarouselModule ], - declarations: [ NzTestCarouselBasicComponent ] + imports: [NzCarouselModule], + declarations: [NzTestCarouselBasicComponent] }); TestBed.compileComponents(); })); @@ -36,7 +36,7 @@ describe('carousel', () => { fixture.detectChanges(); expect(carouselWrapper.nativeElement.classList).toContain('ant-carousel'); expect(carouselContents.every(content => content.nativeElement.classList.contains('slick-slide'))).toBe(true); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); }); it('should dynamic change content work', fakeAsync(() => { @@ -67,35 +67,37 @@ describe('carousel', () => { expect(carouselWrapper.nativeElement.querySelector('.slick-dots').children.length).toBe(4); expect(carouselWrapper.nativeElement.querySelector('.slick-dots').firstElementChild.innerText).toBe('1'); expect(carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.innerText).toBe('4'); - expect(carouselWrapper.nativeElement.querySelector('.slick-dots').firstElementChild.firstElementChild.tagName).toBe('A'); + expect( + carouselWrapper.nativeElement.querySelector('.slick-dots').firstElementChild.firstElementChild.tagName + ).toBe('A'); }); it('should click content change', () => { fixture.detectChanges(); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); fixture.detectChanges(); - expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[3].nativeElement.classList).toContain('slick-active'); }); it('should keydown change content work', fakeAsync(() => { fixture.detectChanges(); const list = carouselWrapper.nativeElement.querySelector('.slick-list'); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); dispatchKeyboardEvent(list, 'keydown', LEFT_ARROW); tickATransition(fixture); - expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[3].nativeElement.classList).toContain('slick-active'); dispatchKeyboardEvent(list, 'keydown', LEFT_ARROW); tickATransition(fixture); - expect(carouselContents[ 2 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[2].nativeElement.classList).toContain('slick-active'); dispatchKeyboardEvent(list, 'keydown', RIGHT_ARROW); tickATransition(fixture); - expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[3].nativeElement.classList).toContain('slick-active'); dispatchKeyboardEvent(list, 'keydown', RIGHT_ARROW); tickATransition(fixture); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); })); it('should vertical work', () => { @@ -116,7 +118,9 @@ describe('carousel', () => { fixture.detectChanges(); tick(1000); fixture.detectChanges(); - expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).toBe('translate3d(0px, 0px, 0px)'); + expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).toBe( + 'translate3d(0px, 0px, 0px)' + ); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); tickATransition(fixture); expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).not.toBe(''); @@ -124,64 +128,66 @@ describe('carousel', () => { testComponent.effect = 'fade'; testComponent.vertical = true; fixture.detectChanges(); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); tickATransition(fixture); expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).toBe(''); testComponent.effect = 'scrollx'; fixture.detectChanges(); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); tickATransition(fixture); - expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).not.toBe('translate3d(0px, 0px, 0px)'); + expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).not.toBe( + 'translate3d(0px, 0px, 0px)' + ); })); it('should autoplay work', fakeAsync(() => { testComponent.autoPlay = true; fixture.detectChanges(); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); fixture.detectChanges(); tick(5000); fixture.detectChanges(); - expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); fixture.detectChanges(); tick(5000); fixture.detectChanges(); testComponent.autoPlay = false; fixture.detectChanges(); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); })); it('should autoplay speed work', fakeAsync(() => { testComponent.autoPlay = true; testComponent.autoPlaySpeed = 1000; fixture.detectChanges(); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); fixture.detectChanges(); tick(1000 + 10); fixture.detectChanges(); - expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); testComponent.autoPlaySpeed = 0; fixture.detectChanges(); tick(2000 + 10); fixture.detectChanges(); - expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); })); it('should func work', fakeAsync(() => { fixture.detectChanges(); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); testComponent.nzCarouselComponent.next(); tickATransition(fixture); - expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); testComponent.nzCarouselComponent.pre(); tickATransition(fixture); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); testComponent.nzCarouselComponent.goTo(2); tickATransition(fixture); - expect(carouselContents[ 2 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[2].nativeElement.classList).toContain('slick-active'); })); it('should resize content after window resized', fakeAsync(() => { @@ -194,21 +200,21 @@ describe('carousel', () => { it('should support swiping to switch', fakeAsync(() => { swipe(testComponent.nzCarouselComponent, 500); tickATransition(fixture); - expect(carouselContents[ 0 ].nativeElement.classList).not.toContain('slick-active'); - expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).not.toContain('slick-active'); + expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); swipe(testComponent.nzCarouselComponent, -500); tickATransition(fixture); swipe(testComponent.nzCarouselComponent, -500); tickATransition(fixture); - expect(carouselContents[ 0 ].nativeElement.classList).not.toContain('slick-active'); - expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).not.toContain('slick-active'); + expect(carouselContents[3].nativeElement.classList).toContain('slick-active'); })); it('should prevent swipes that are not long enough', fakeAsync(() => { swipe(testComponent.nzCarouselComponent, 2); tickATransition(fixture); - expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); })); }); @@ -281,13 +287,12 @@ describe('carousel', () => { testComponent.nzCarouselComponent.goTo(1); swipe(testComponent.nzCarouselComponent, 500); tickATransition(fixture); - expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); })); }); // Already covered in components specs. - describe('opacity strategy', () => { - }); + describe('opacity strategy', () => {}); }); }); @@ -302,17 +307,23 @@ describe('carousel', () => { [nzAutoPlay]="autoPlay" [nzAutoPlaySpeed]="autoPlaySpeed" (nzAfterChange)="afterChange($event)" - (nzBeforeChange)="beforeChange($event)"> -

{{index}}

- {{index + 1}} - ` + (nzBeforeChange)="beforeChange($event)" + > +
+

{{ index }}

+
+ {{ index + 1 }} + + ` }) export class NzTestCarouselBasicComponent { @ViewChild(NzCarouselComponent) nzCarouselComponent: NzCarouselComponent; dots = true; vertical = false; effect = 'scrollx'; - array = [ 1, 2, 3, 4 ]; + array = [1, 2, 3, 4]; autoPlay = false; autoPlaySpeed = 3000; afterChange = jasmine.createSpy('afterChange callback'); diff --git a/components/icon/nz-icon.directive.ts b/components/icon/nz-icon.directive.ts index 7d88165f4fe..e31fa2992da 100644 --- a/components/icon/nz-icon.directive.ts +++ b/components/icon/nz-icon.directive.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { AfterContentChecked, Directive, @@ -171,7 +172,12 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges, } } - constructor(public iconService: NzIconService, public elementRef: ElementRef, public renderer: Renderer2) { + constructor( + public iconService: NzIconService, + public elementRef: ElementRef, + public renderer: Renderer2, + private platform: Platform + ) { super(iconService, elementRef, renderer); } @@ -193,13 +199,15 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges, this.iconService.warnAPI('old'); // Get `type` from `className`. If not, initial rendering would be missed. this.classChangeHandler(this.el.className); - // Add `class` mutation observer. - this.classNameObserver = new MutationObserver((mutations: MutationRecord[]) => { - mutations - .filter((mutation: MutationRecord) => mutation.attributeName === 'class') - .forEach((mutation: MutationRecord) => this.classChangeHandler((mutation.target as HTMLElement).className)); - }); - this.classNameObserver.observe(this.el, { attributes: true }); + if (this.platform.isBrowser) { + // Add `class` mutation observer. + this.classNameObserver = new MutationObserver((mutations: MutationRecord[]) => { + mutations + .filter((mutation: MutationRecord) => mutation.attributeName === 'class') + .forEach((mutation: MutationRecord) => this.classChangeHandler((mutation.target as HTMLElement).className)); + }); + this.classNameObserver.observe(this.el, { attributes: true }); + } } // If `classList` does not contain `anticon`, add it before other class names. if (!this.el.classList.contains('anticon')) { diff --git a/components/menu/nz-submenu.component.ts b/components/menu/nz-submenu.component.ts index fb7577181e4..097c922205b 100644 --- a/components/menu/nz-submenu.component.ts +++ b/components/menu/nz-submenu.component.ts @@ -1,4 +1,5 @@ import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; +import { Platform } from '@angular/cdk/platform'; import { AfterContentInit, ChangeDetectionStrategy, @@ -107,7 +108,7 @@ export class NzSubMenuComponent implements OnInit, OnDestroy, AfterContentInit, } setTriggerWidth(): void { - if (this.nzSubmenuService.mode === 'horizontal') { + if (this.nzSubmenuService.mode === 'horizontal' && this.platform.isBrowser) { this.triggerWidth = this.cdkOverlayOrigin.nativeElement.getBoundingClientRect().width; } } @@ -135,6 +136,7 @@ export class NzSubMenuComponent implements OnInit, OnDestroy, AfterContentInit, private cdr: ChangeDetectorRef, public nzSubmenuService: NzSubmenuService, private nzUpdateHostClassService: NzUpdateHostClassService, + private platform: Platform, @Host() @Optional() public noAnimation?: NzNoAnimationDirective ) {} diff --git a/components/select/nz-select.component.ts b/components/select/nz-select.component.ts index fe84323c969..838ed54bfb5 100644 --- a/components/select/nz-select.component.ts +++ b/components/select/nz-select.component.ts @@ -1,5 +1,6 @@ import { FocusMonitor } from '@angular/cdk/a11y'; import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; +import { Platform } from '@angular/cdk/platform'; import { forwardRef, AfterContentInit, @@ -232,7 +233,9 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterVie } updateCdkConnectedOverlayStatus(): void { - this.triggerWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width; + if (this.platform.isBrowser) { + this.triggerWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width; + } } updateCdkConnectedOverlayPositions(): void { @@ -248,6 +251,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterVie public nzSelectService: NzSelectService, private cdr: ChangeDetectorRef, private focusMonitor: FocusMonitor, + private platform: Platform, elementRef: ElementRef, @Host() @Optional() public noAnimation?: NzNoAnimationDirective ) { diff --git a/components/slider/nz-slider.component.ts b/components/slider/nz-slider.component.ts index bc54b474ad0..bed5c40bd49 100644 --- a/components/slider/nz-slider.component.ts +++ b/components/slider/nz-slider.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { forwardRef, ChangeDetectionStrategy, @@ -91,13 +92,15 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange private dragMove_: Subscription | null; private dragEnd_: Subscription | null; - constructor(private cdr: ChangeDetectorRef) {} + constructor(private cdr: ChangeDetectorRef, private platform: Platform) {} ngOnInit(): void { this.handles = this.generateHandles(this.nzRange ? 2 : 1); this.sliderDOM = this.slider.nativeElement; this.marksArray = this.nzMarks ? this.generateMarkItems(this.nzMarks) : null; - this.createDraggingObservables(); + if (this.platform.isBrowser) { + this.createDraggingObservables(); + } this.toggleDragDisabled(this.nzDisabled); if (this.getValue() === null) { diff --git a/components/statistic/nz-countdown.component.ts b/components/statistic/nz-countdown.component.ts index 8fa0e77dcb7..257e158055e 100644 --- a/components/statistic/nz-countdown.component.ts +++ b/components/statistic/nz-countdown.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -31,7 +32,7 @@ export class NzCountdownComponent extends NzStatisticComponent implements OnInit private target: number; private updater_: Subscription | null; - constructor(private cdr: ChangeDetectorRef, private ngZone: NgZone) { + constructor(private cdr: ChangeDetectorRef, private ngZone: NgZone, private platform: Platform) { super(); } @@ -62,13 +63,15 @@ export class NzCountdownComponent extends NzStatisticComponent implements OnInit } startTimer(): void { - this.ngZone.runOutsideAngular(() => { - this.stopTimer(); - this.updater_ = interval(REFRESH_INTERVAL).subscribe(() => { - this.updateValue(); - this.cdr.detectChanges(); + if (this.platform.isBrowser) { + this.ngZone.runOutsideAngular(() => { + this.stopTimer(); + this.updater_ = interval(REFRESH_INTERVAL).subscribe(() => { + this.updateValue(); + this.cdr.detectChanges(); + }); }); - }); + } } stopTimer(): void { diff --git a/components/table/nz-table.component.ts b/components/table/nz-table.component.ts index 1b2c7aed803..3743b588a8c 100644 --- a/components/table/nz-table.component.ts +++ b/components/table/nz-table.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { AfterContentInit, @@ -217,6 +218,7 @@ export class NzTableComponent implements OnInit, AfterViewInit, OnDestr private cdr: ChangeDetectorRef, private nzMeasureScrollbarService: NzMeasureScrollbarService, private i18n: NzI18nService, + private platform: Platform, elementRef: ElementRef ) { renderer.addClass(elementRef.nativeElement, 'ant-table-wrapper'); @@ -244,6 +246,9 @@ export class NzTableComponent implements OnInit, AfterViewInit, OnDestr } ngAfterViewInit(): void { + if (!this.platform.isBrowser) { + return; + } setTimeout(() => this.setScrollPositionClassName()); this.ngZone.runOutsideAngular(() => { merge( diff --git a/components/timeline/nz-timeline.component.ts b/components/timeline/nz-timeline.component.ts index 66631846f9d..0cfbc4d6435 100644 --- a/components/timeline/nz-timeline.component.ts +++ b/components/timeline/nz-timeline.component.ts @@ -1,3 +1,4 @@ +import { Platform } from '@angular/cdk/platform'; import { AfterContentInit, ChangeDetectionStrategy, @@ -46,7 +47,7 @@ export class NzTimelineComponent implements AfterContentInit, OnChanges, OnDestr private destroy$ = new Subject(); - constructor(private cdr: ChangeDetectorRef) {} + constructor(private cdr: ChangeDetectorRef, private platform: Platform) {} ngOnChanges(changes: SimpleChanges): void { const modeChanges = changes.nzMode; @@ -102,7 +103,9 @@ export class NzTimelineComponent implements AfterContentInit, OnChanges, OnDestr } private reverseChildTimelineDots(): void { - reverseChildNodes(this.timeline.nativeElement as HTMLElement); - this.updateChildren(); + if (this.platform.isBrowser) { + reverseChildNodes(this.timeline.nativeElement as HTMLElement); + this.updateChildren(); + } } } diff --git a/components/upload/nz-upload-list.component.ts b/components/upload/nz-upload-list.component.ts index 2033625e93f..b289ebd3ba4 100644 --- a/components/upload/nz-upload-list.component.ts +++ b/components/upload/nz-upload-list.component.ts @@ -1,4 +1,5 @@ import { animate, style, transition, trigger } from '@angular/animations'; +import { Platform } from '@angular/cdk/platform'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -115,6 +116,9 @@ export class NzUploadListComponent implements OnChanges { } private genThumb(): void { + if (!this.platform.isBrowser) { + return; + } // tslint:disable-next-line:no-any const win = window as any; if ( @@ -167,7 +171,8 @@ export class NzUploadListComponent implements OnChanges { constructor( private el: ElementRef, private cdr: ChangeDetectorRef, - private updateHostClassService: NzUpdateHostClassService + private updateHostClassService: NzUpdateHostClassService, + private platform: Platform ) {} detectChanges(): void { diff --git a/docs/animations.en-US.md b/docs/animations.en-US.md index b8fc2795cb9..6795c349bde 100644 --- a/docs/animations.en-US.md +++ b/docs/animations.en-US.md @@ -1,5 +1,5 @@ --- -order: 6 +order: 7 title: Animations Switch --- diff --git a/docs/animations.zh-CN.md b/docs/animations.zh-CN.md index f9f26932733..f4ba9762ed2 100644 --- a/docs/animations.zh-CN.md +++ b/docs/animations.zh-CN.md @@ -1,5 +1,5 @@ --- -order: 6 +order: 7 title: 动画开关 --- diff --git a/docs/changelog.en-US.md b/docs/changelog.en-US.md index c4a61032aad..df2da98ac28 100755 --- a/docs/changelog.en-US.md +++ b/docs/changelog.en-US.md @@ -1,5 +1,5 @@ --- -order: 9 +order: 11 title: Change Log toc: false timeline: true diff --git a/docs/changelog.zh-CN.md b/docs/changelog.zh-CN.md index 5927ad877d1..5416d334f20 100755 --- a/docs/changelog.zh-CN.md +++ b/docs/changelog.zh-CN.md @@ -1,5 +1,5 @@ --- -order: 9 +order: 11 title: 更新日志 toc: false timeline: true diff --git a/docs/contributing.en-US.md b/docs/contributing.en-US.md index ca54bdc0331..08f96f51d05 100644 --- a/docs/contributing.en-US.md +++ b/docs/contributing.en-US.md @@ -1,5 +1,5 @@ --- -order: 9 +order: 10 title: Contributing to NG-ZORRO --- diff --git a/docs/contributing.zh-CN.md b/docs/contributing.zh-CN.md index 6e4c2299672..4f2a9ca9ea7 100644 --- a/docs/contributing.zh-CN.md +++ b/docs/contributing.zh-CN.md @@ -1,5 +1,5 @@ --- -order: 9 +order: 10 title: 贡献指南 --- diff --git a/docs/customize-theme.en-US.md b/docs/customize-theme.en-US.md index e383389f5e4..7ad8173381f 100644 --- a/docs/customize-theme.en-US.md +++ b/docs/customize-theme.en-US.md @@ -1,5 +1,5 @@ --- -order: 5 +order: 6 title: Customize Theme --- diff --git a/docs/customize-theme.zh-CN.md b/docs/customize-theme.zh-CN.md index 4fb833d3f21..e234713d8b5 100644 --- a/docs/customize-theme.zh-CN.md +++ b/docs/customize-theme.zh-CN.md @@ -1,5 +1,5 @@ --- -order: 5 +order: 6 title: 定制主题 --- diff --git a/docs/faq.en-US.md b/docs/faq.en-US.md index c15c042fb7a..943f6099c4f 100644 --- a/docs/faq.en-US.md +++ b/docs/faq.en-US.md @@ -1,5 +1,5 @@ --- -order: 8 +order: 9 title: FAQ --- diff --git a/docs/faq.zh-CN.md b/docs/faq.zh-CN.md index 9991693180e..155c96f9c81 100644 --- a/docs/faq.zh-CN.md +++ b/docs/faq.zh-CN.md @@ -1,5 +1,5 @@ --- -order: 8 +order: 9 title: 常见问题 --- diff --git a/docs/recommendation.en-US.md b/docs/recommendation.en-US.md index 922dd1a2644..a533a2e67c9 100644 --- a/docs/recommendation.en-US.md +++ b/docs/recommendation.en-US.md @@ -1,5 +1,5 @@ --- -order: 7 +order: 8 title: Third-Party Libraries --- diff --git a/docs/recommendation.zh-CN.md b/docs/recommendation.zh-CN.md index 81682be4e46..b3728d30e46 100644 --- a/docs/recommendation.zh-CN.md +++ b/docs/recommendation.zh-CN.md @@ -1,5 +1,5 @@ --- -order: 7 +order: 8 title: 社区推荐 --- diff --git a/docs/universal.en-US.md b/docs/universal.en-US.md new file mode 100644 index 00000000000..2fe4f1b035b --- /dev/null +++ b/docs/universal.en-US.md @@ -0,0 +1,115 @@ +--- +order: 5 +title: Server-side Rendering +--- + +

This guide assumes that you already know about Server-side Rendering(SSR)

+ +This guide is based on the popular [Express](https://expressjs.com/) framework. + +## Installation dependence + +```bash +$ ng add @nguniversal/express-engine --clientProject +``` + +## Modify the server root module + +**app.server.module.ts** + +```ts +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; + +// Import the require modules +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientModule } from '@angular/common/http'; +import { en_US, NZ_I18N, NzI18nModule } from 'ng-zorro-antd/i18n'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ModuleMapLoaderModule, + HttpClientModule, + NoopAnimationsModule, + NzI18nModule + ], + bootstrap: [AppComponent], + providers: [ + { provide: NZ_I18N, useValue: en_US } + ] +}) +export class AppServerModule {} + +``` + +## Modify the server environment + +**server.ts** + +```ts +import 'zone.js/dist/zone-node'; +import {enableProdMode} from '@angular/core'; +// Express Engine +import {ngExpressEngine} from '@nguniversal/express-engine'; +// Import module map for lazy loading +import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader'; + +import * as express from 'express'; +import {join} from 'path'; + +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); + +// Fix the `Event is not defined` error https://github.com/angular/universal/issues/844 +global['Event'] = null; + +// Express server +const app = express(); + +const PORT = process.env.PORT || 4000; +const DIST_FOLDER = join(process.cwd(), 'dist/browser'); + +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main'); + +// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) +app.engine('html', ngExpressEngine({ + bootstrap: AppServerModuleNgFactory, + providers: [ + provideModuleMap(LAZY_MODULE_MAP) + ] +})); + +app.set('view engine', 'html'); +app.set('views', DIST_FOLDER); + +// Example Express Rest API endpoints +// app.get('/api/**', (req, res) => { }); +// Serve static files from /browser +app.get('*.*', express.static(DIST_FOLDER, { + maxAge: '1y' +})); + +// All regular routes use the Universal engine +app.get('*', (req, res) => { + res.render('index', { req }); +}); + +// Start up the Node server +app.listen(PORT, () => { + console.log(`Node Express server listening on http://localhost:${PORT}`); +}); + +``` + +## Compile and run + +```bash +$ npm run build:ssr && npm run serve:ssr # Running http://localhost:4000/ +``` \ No newline at end of file diff --git a/docs/universal.zh-CN.md b/docs/universal.zh-CN.md new file mode 100644 index 00000000000..d000668c8e2 --- /dev/null +++ b/docs/universal.zh-CN.md @@ -0,0 +1,115 @@ +--- +order: 5 +title: 服务端渲染 +--- + +

本指南假设你已了解关于 Angular Universal:服务端渲染 的相关知识!

+ +本指南 Web 服务器是基于常见的 [Express](https://expressjs.com/) 框架。 + +## 安装依赖 + +```bash +$ ng add @nguniversal/express-engine --clientProject +``` + +## 修改服务端根模块 + +**app.server.module.ts** + +```ts +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; + +// 引入必要的模块 +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientModule } from '@angular/common/http'; +import { en_US, NZ_I18N, NzI18nModule } from 'ng-zorro-antd/i18n'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ModuleMapLoaderModule, + HttpClientModule, + NoopAnimationsModule, + NzI18nModule + ], + bootstrap: [AppComponent], + providers: [ + { provide: NZ_I18N, useValue: en_US } + ] +}) +export class AppServerModule {} + +``` + +## 修改服务器环境 + +**server.ts** + +```ts +import 'zone.js/dist/zone-node'; +import {enableProdMode} from '@angular/core'; +// Express Engine +import {ngExpressEngine} from '@nguniversal/express-engine'; +// Import module map for lazy loading +import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader'; + +import * as express from 'express'; +import {join} from 'path'; + +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); + +// 修复 `Event is not defined` 的错误 https://github.com/angular/universal/issues/844 +global['Event'] = null; + +// Express server +const app = express(); + +const PORT = process.env.PORT || 4000; +const DIST_FOLDER = join(process.cwd(), 'dist/browser'); + +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main'); + +// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) +app.engine('html', ngExpressEngine({ + bootstrap: AppServerModuleNgFactory, + providers: [ + provideModuleMap(LAZY_MODULE_MAP) + ] +})); + +app.set('view engine', 'html'); +app.set('views', DIST_FOLDER); + +// Example Express Rest API endpoints +// app.get('/api/**', (req, res) => { }); +// Serve static files from /browser +app.get('*.*', express.static(DIST_FOLDER, { + maxAge: '1y' +})); + +// All regular routes use the Universal engine +app.get('*', (req, res) => { + res.render('index', { req }); +}); + +// Start up the Node server +app.listen(PORT, () => { + console.log(`Node Express server listening on http://localhost:${PORT}`); +}); + +``` + +## 编译运行 + +```bash +$ npm run build:ssr && npm run serve:ssr # 运行在 http://localhost:4000/ +``` \ No newline at end of file