diff --git a/src/components/ng-zorro-antd.module.spec.ts b/src/components/ng-zorro-antd.module.spec.ts new file mode 100644 index 00000000000..8855a5757f6 --- /dev/null +++ b/src/components/ng-zorro-antd.module.spec.ts @@ -0,0 +1,26 @@ +import { async, TestBed } from '@angular/core/testing'; +import { NgZorroAntdModule, NZ_ROOT_CONFIG, NzRootConfig } from './ng-zorro-antd.module'; + +describe('NgZorroAntdModule with Angular integration', () => { + it('should not provide root config with empty forRoot', async(() => { + TestBed.configureTestingModule({ + imports: [ + NgZorroAntdModule.forRoot(), + ], + }).compileComponents(); + + expect(TestBed.get(NZ_ROOT_CONFIG)).not.toBeDefined(); + })); + + it('should provide root config with params in forRoot', async(() => { + const options: NzRootConfig = { extraFontName: '', extraFontUrl: '' }; + + TestBed.configureTestingModule({ + imports: [ + NgZorroAntdModule.forRoot(options), + ], + }).compileComponents(); + + expect(TestBed.get(NZ_ROOT_CONFIG)).toBeDefined(); + })); +}); diff --git a/src/components/ng-zorro-antd.module.ts b/src/components/ng-zorro-antd.module.ts index 5c5ca2cd6ef..15d166a8d9c 100644 --- a/src/components/ng-zorro-antd.module.ts +++ b/src/components/ng-zorro-antd.module.ts @@ -53,6 +53,9 @@ import { NzMessageService } from './message/nz-message.service'; import { NzModalService } from './modal/nz-modal.service'; import { NzModalSubject } from './modal/nz-modal-subject.service'; +// Tokens (eg. global services' config) +import { NZ_ROOT_CONFIG, NzRootConfig } from './root/nz-root-config' + // --------------------------------------------------------- // | Exports // --------------------------------------------------------- @@ -108,6 +111,7 @@ export { NzModalSubject } from './modal/nz-modal-subject.service'; // Tokens (eg. global services' config) export { NZ_MESSAGE_CONFIG } from './message/nz-message-config'; export { NZ_NOTIFICATION_CONFIG } from './notification/nz-notification-config'; +export { NZ_ROOT_CONFIG, NzRootConfig } from './root/nz-root-config'; // --------------------------------------------------------- // | Root module @@ -159,13 +163,14 @@ export { NZ_NOTIFICATION_CONFIG } from './notification/nz-notification-config'; }) export class NgZorroAntdModule { - static forRoot(): ModuleWithProviders { + static forRoot(options?: NzRootConfig): ModuleWithProviders { return { ngModule: NgZorroAntdModule, providers: [ // Services NzNotificationService, - NzMessageService + NzMessageService, + { provide: NZ_ROOT_CONFIG, useValue: options }, ] }; } diff --git a/src/components/root/nz-root-config.spec.ts b/src/components/root/nz-root-config.spec.ts new file mode 100644 index 00000000000..99947c62e0f --- /dev/null +++ b/src/components/root/nz-root-config.spec.ts @@ -0,0 +1,32 @@ +import { createNzRootInitializer, NzRootConfig } from './nz-root-config'; + +describe('NzRootConfig', () => { + let mockDocument: Document; + let mockConfig: NzRootConfig; + let mockElement: HTMLDivElement; + + beforeEach(() => { + mockDocument = { head: { appendChild: () => null }, createElement: () => null } as any; + mockConfig = { extraFontName: '', extraFontUrl: '' } as any; + mockElement = {} as any; + + spyOn(mockDocument, 'createElement').and.returnValue(mockElement); + spyOn(mockDocument.head, 'appendChild'); + }); + + it('should apply extra font style when option provided', () => { + const iniliatizer = createNzRootInitializer(mockDocument, mockConfig); + iniliatizer(); + + expect(mockDocument.createElement).toHaveBeenCalledWith('style'); + expect(mockDocument.head.appendChild).toHaveBeenCalledWith(mockElement); + }); + + it('should not apply extra font style when option not provided', () => { + const iniliatizer = createNzRootInitializer(mockDocument); + iniliatizer(); + + expect(mockDocument.createElement).not.toHaveBeenCalled(); + expect(mockDocument.head.appendChild).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/root/nz-root-config.ts b/src/components/root/nz-root-config.ts new file mode 100644 index 00000000000..e3d56c0de61 --- /dev/null +++ b/src/components/root/nz-root-config.ts @@ -0,0 +1,32 @@ +import { InjectionToken } from '@angular/core'; + +export interface NzRootConfig { + extraFontName: string; + extraFontUrl: string; +} + +export const NZ_ROOT_CONFIG = new InjectionToken('NzRootConfig'); + +export function createNzRootInitializer(document: Document, options?: NzRootConfig) { + return function nzRootInitializer() { + if (options) { + const style = document.createElement('style'); + style.innerHTML = ` + @font-face { + font-family: '${options.extraFontName}'; + src: url('${options.extraFontUrl}.eot'); /* IE9*/ + src: + /* IE6-IE8 */ + url('${options.extraFontUrl}.eot?#iefix') format('embedded-opentype'), + /* chrome、firefox */ + url('${options.extraFontUrl}.woff') format('woff'), + /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ + url('${options.extraFontUrl}.ttf') format('truetype'), + /* iOS 4.1- */ + url('${options.extraFontUrl}.svg#iconfont') format('svg'); + } + `; + document.head.appendChild(style); + } + } +} diff --git a/src/components/root/nz-root-style.component.ts b/src/components/root/nz-root-style.component.ts new file mode 100644 index 00000000000..799536d13c5 --- /dev/null +++ b/src/components/root/nz-root-style.component.ts @@ -0,0 +1,11 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + template : ``, + styleUrls : [ + '../style/index.less', + './style/index.less', + ], + encapsulation : ViewEncapsulation.None, +}) +export class NzRootStyleComponent { } diff --git a/src/components/root/nz-root.component.spec.ts b/src/components/root/nz-root.component.spec.ts new file mode 100644 index 00000000000..b07898f2d7e --- /dev/null +++ b/src/components/root/nz-root.component.spec.ts @@ -0,0 +1,52 @@ +import { ComponentFactoryResolver, Injector, ComponentRef, ComponentFactory, APP_INITIALIZER } from '@angular/core'; +import { async, inject, TestBed } from '@angular/core/testing'; +import { DOCUMENT } from '@angular/common'; +import { NzRootComponent } from './nz-root.component'; +import { NZ_ROOT_CONFIG, NzRootConfig } from './nz-root-config'; + +describe('NzRootComponent', () => { + let component: NzRootComponent; + let mockDocument: Document; + let mockConfig: NzRootConfig; + let mockElement: HTMLDivElement; + + beforeEach(() => { + mockDocument = { head: { appendChild: () => null }, createElement: () => null } as any; + mockConfig = { extraFontName: '', extraFontUrl: '' } as any; + mockElement = {} as any; + + spyOn(mockDocument, 'createElement').and.returnValue(mockElement); + spyOn(mockDocument.head, 'appendChild'); + }); + + it('should apply extra font style when input being set & option not provided', () => { + component = new NzRootComponent(mockDocument, undefined); + component.nzExtraFontName = 'some-name'; + component.nzExtraFontUrl = 'some-url'; + + component.ngOnInit(); + + expect(mockDocument.createElement).toHaveBeenCalledWith('style'); + expect(mockDocument.head.appendChild).toHaveBeenCalledWith(mockElement); + }); + + it('should not apply extra font style when option being provided', () => { + component = new NzRootComponent(mockDocument, mockConfig); + component.nzExtraFontName = 'some-name'; + component.nzExtraFontUrl = 'some-url'; + + component.ngOnInit(); + + expect(mockDocument.createElement).not.toHaveBeenCalled(); + expect(mockDocument.head.appendChild).not.toHaveBeenCalled(); + }); + + it('should not apply extra font style when option being provided', () => { + component = new NzRootComponent(mockDocument, undefined); + + component.ngOnInit(); + + expect(mockDocument.createElement).not.toHaveBeenCalled(); + expect(mockDocument.head.appendChild).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/root/nz-root.component.ts b/src/components/root/nz-root.component.ts index a9163a0b2a1..1281f274ffd 100644 --- a/src/components/root/nz-root.component.ts +++ b/src/components/root/nz-root.component.ts @@ -1,22 +1,18 @@ import { Component, Input, - ViewEncapsulation, OnInit, - Inject + Inject, + Optional, } from '@angular/core'; import { DOCUMENT } from '@angular/common'; +import { NZ_ROOT_CONFIG, NzRootConfig, createNzRootInitializer} from './nz-root-config'; @Component({ - selector : '[nz-root],nz-root', - encapsulation: ViewEncapsulation.None, - template : ` + selector : '[nz-root],nz-root', + template : ` `, - styleUrls : [ - '../style/index.less', - './style/index.less', - ] }) export class NzRootComponent implements OnInit { @@ -24,27 +20,18 @@ export class NzRootComponent implements OnInit { @Input() nzExtraFontName: string; @Input() nzExtraFontUrl: string; - constructor(@Inject(DOCUMENT) private _document: Document) { } + constructor( + @Inject(DOCUMENT) private _document: Document, + // Cannot use type annotation here due to https://github.com/angular/angular-cli/issues/2034 + // Should be revisited after AOT being made the only option + @Inject(NZ_ROOT_CONFIG) @Optional() private options: any | undefined, + ) { } ngOnInit() { - if (this.nzExtraFontName && this.nzExtraFontUrl) { - const style = this._document.createElement('style'); - style.innerHTML = ` - @font-face { - font-family: '${this.nzExtraFontName}'; - src: url('${this.nzExtraFontUrl}.eot'); /* IE9*/ - src: - /* IE6-IE8 */ - url('${this.nzExtraFontUrl}.eot?#iefix') format('embedded-opentype'), - /* chrome、firefox */ - url('${this.nzExtraFontUrl}.woff') format('woff'), - /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ - url('${this.nzExtraFontUrl}.ttf') format('truetype'), - /* iOS 4.1- */ - url('${this.nzExtraFontUrl}.svg#iconfont') format('svg'); - } - `; - this._document.head.appendChild(style); + if (this.nzExtraFontName && this.nzExtraFontUrl && !this.options) { + const options: NzRootConfig = { extraFontName: this.nzExtraFontName, extraFontUrl: this.nzExtraFontUrl }; + const initializer = createNzRootInitializer(this._document, options); + initializer(); } } } diff --git a/src/components/root/nz-root.module.spec.ts b/src/components/root/nz-root.module.spec.ts new file mode 100644 index 00000000000..674196fc3ee --- /dev/null +++ b/src/components/root/nz-root.module.spec.ts @@ -0,0 +1,58 @@ +import { ComponentFactoryResolver, Injector, ComponentRef, ComponentFactory, APP_INITIALIZER } from '@angular/core'; +import { async, inject, TestBed } from '@angular/core/testing'; +import { NzRootModule } from './nz-root.module'; +import { NzRootStyleComponent } from './nz-root-style.component'; + +describe('NzRootModule', () => { + let ngModule: NzRootModule; + let mockDocument: Document; + let mockInjector: Injector; + let mockFactoryResolver: ComponentFactoryResolver; + let mockElement: HTMLDivElement; + let mockComponentFactory: ComponentFactory; + let mockComponentRef: ComponentRef; + + beforeEach(() => { + mockDocument = { createElement: () => null } as any; + mockInjector = {} as any; + mockFactoryResolver = { resolveComponentFactory: () => null } as any; + mockElement = {} as any; + mockComponentFactory = { create: () => null } as any; + mockComponentRef = { destroy: () => null } as any; + + spyOn(mockDocument, 'createElement').and.returnValue(mockElement); + spyOn(mockComponentRef, 'destroy'); + spyOn(mockComponentFactory, 'create').and.returnValue(mockComponentRef); + spyOn(mockFactoryResolver, 'resolveComponentFactory').and.returnValue(mockComponentFactory); + }); + + beforeEach(() => { + ngModule = new NzRootModule(mockDocument, mockInjector, mockFactoryResolver); + }); + + it('should create style component when start', () => { + expect(mockDocument.createElement).toHaveBeenCalledWith('div'); + expect(mockFactoryResolver.resolveComponentFactory).toHaveBeenCalledWith(NzRootStyleComponent); + expect(mockComponentFactory.create).toHaveBeenCalledWith(mockInjector, null, mockElement); + }); + + it('should destroy style component when terminate', () => { + ngModule.ngOnDestroy(); + + expect(mockComponentRef.destroy).toHaveBeenCalled(); + }) +}); + +describe('NzRootModule with Angular integration', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [NzRootModule], + declarations: [], + providers: [], + }).compileComponents(); + })); + + it('should provide APP_INITIALIZER', inject([APP_INITIALIZER], (initializers: Function[]) => { + expect(initializers.some(x => x.name === 'nzRootInitializer')).toBe(true); + })); +}); diff --git a/src/components/root/nz-root.module.ts b/src/components/root/nz-root.module.ts index 24f7e4cf23b..f37156ae6b3 100644 --- a/src/components/root/nz-root.module.ts +++ b/src/components/root/nz-root.module.ts @@ -1,12 +1,28 @@ -import { NgModule } from '@angular/core'; +import { NgModule, OnDestroy, ComponentRef, ComponentFactoryResolver, Inject, Optional, Injector, APP_INITIALIZER } from '@angular/core'; +import { CommonModule, DOCUMENT } from '@angular/common'; import { NzRootComponent } from './nz-root.component'; -import { CommonModule } from '@angular/common'; +import { NzRootStyleComponent } from './nz-root-style.component'; +import { NZ_ROOT_CONFIG, createNzRootInitializer } from './nz-root-config'; @NgModule({ - exports : [ NzRootComponent ], - declarations: [ NzRootComponent ], - imports : [ CommonModule ] + exports : [ NzRootComponent ], + declarations : [ NzRootComponent, NzRootStyleComponent ], + imports : [ CommonModule ], + entryComponents : [ NzRootStyleComponent ], + providers : [ + { provide: APP_INITIALIZER, multi: true, useFactory: createNzRootInitializer, deps: [DOCUMENT, [new Optional(), NZ_ROOT_CONFIG]] }, + ], }) +export class NzRootModule implements OnDestroy { + private styleHostComponent: ComponentRef; -export class NzRootModule { + constructor(@Inject(DOCUMENT) _document: Document, injector: Injector, resolver: ComponentFactoryResolver) { + const componentFactory = resolver.resolveComponentFactory(NzRootStyleComponent); + const div = _document.createElement('div'); + this.styleHostComponent = componentFactory.create(injector, null, div); + } + + ngOnDestroy() { + this.styleHostComponent.destroy(); + } }