diff --git a/config/karma-test-shim.js b/config/karma-test-shim.js index a2fb099eb4..9adc817cfd 100644 --- a/config/karma-test-shim.js +++ b/config/karma-test-shim.js @@ -38,14 +38,16 @@ System.import('system-config.js').then(function() { System.import('@angular/router'), System.import('@angular/http'), System.import('@angular/forms'), - System.import('@angular2-material/icon') + System.import('@angular/core'), + System.import('@angular2-material/icon'), ]).then(function (providers) { var testing = providers[0]; var testingBrowser = providers[1]; var testingRouter = providers[2]; var testingHttp = providers[3]; var testingForms = providers[4]; - var testingIcon = providers[5]; + var testingCore = providers[5]; + var testingIcon = providers[6]; testing.setBaseTestProviders(testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS ); @@ -59,6 +61,7 @@ System.import('system-config.js').then(function() { testingIcon.MdIconRegistry, { provide: testingRouter.Router, useValue: {} }, { provide: testingRouter.ActivatedRoute, useValue: {} }, + testingCore.ChangeDetectorRef, ]); }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f1c64c82e2..32417ea0ba 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,7 +4,7 @@ import { ROUTER_DIRECTIVES } from '@angular/router'; import { MdIcon } from '@angular2-material/icon'; import { MD_LIST_DIRECTIVES } from '@angular2-material/list'; -import { TdLayoutComponent } from '../platform/core'; +import { TdLayoutComponent, TD_LOADING_ENTRY_COMPONENTS } from '../platform/core'; @Component({ directives: [ @@ -17,6 +17,7 @@ import { TdLayoutComponent } from '../platform/core'; selector: 'td-docs-app', styleUrls: ['app.component.css'], templateUrl: 'app.component.html', + precompile: [ TD_LOADING_ENTRY_COMPONENTS ], }) export class DocsAppComponent { diff --git a/src/app/components/components/loading/loading.component.html b/src/app/components/components/loading/loading.component.html index 1babdfbd8b..66cb1f643f 100644 --- a/src/app/components/components/loading/loading.component.html +++ b/src/app/components/components/loading/loading.component.html @@ -38,6 +38,7 @@

tdLoading

Simply add the tdLoading attibute with a "name" value to the element you want to mask.

Dont forget to add the asterisk syntax before the tdLoading directive if its not used in a ]]> element.

More info on the asterisk (*) syntax here

+

Note: when used on load, should be registered in [TdLoadingService] after 'AfterViewInit#ngAfterViewInit()' component hook cycle.

Properties:

The tdLoading component has {{loadingAttrs.length}} properties:

@@ -69,10 +70,11 @@

Example:

Typescript:

Example: { let builder: TestComponentBuilder; @@ -21,6 +21,11 @@ describe('Component: LoadingDemo', () => { Injector, ViewContainerRef, TdLoadingService, + { provide: ComponentFactoryResolver, useValue: {resolveComponentFactory: function(): any{ + return {create: function(): any{ + return {instance: {}}; + }}; + }}}, ]); }); @@ -48,6 +53,7 @@ describe('Component: LoadingDemo', () => { template: ` `, + precompile: [ TD_LOADING_ENTRY_COMPONENTS ], }) class LoadingDemoTestControllerComponent { } diff --git a/src/app/components/components/loading/loading.component.ts b/src/app/components/components/loading/loading.component.ts index db7ec5c0c4..09304fa039 100644 --- a/src/app/components/components/loading/loading.component.ts +++ b/src/app/components/components/loading/loading.component.ts @@ -1,5 +1,4 @@ -import { Component, ViewContainerRef } from '@angular/core'; -import { TimerWrapper } from '@angular/core/src/facade/async'; +import { Component, ViewContainerRef, AfterViewInit, ChangeDetectorRef } from '@angular/core'; import { MD_CARD_DIRECTIVES } from '@angular2-material/card'; import { MD_LIST_DIRECTIVES } from '@angular2-material/list'; @@ -24,7 +23,7 @@ import { TdHighlightComponent } from '../../../../platform/highlight'; styleUrls: [ 'loading.component.css' ], templateUrl: 'loading.component.html', }) -export class LoadingDemoComponent { +export class LoadingDemoComponent implements AfterViewInit { demo: {name?: string, description?: string} = {}; demo2: {name?: string, description?: string} = {}; @@ -41,13 +40,15 @@ export class LoadingDemoComponent { }]; loadingServiceMethods: Object[] = [{ - description: 'Registers a request for the loading mask referenced by the name parameter.', + description: `Registers a request for the loading mask referenced by the name parameter. + Can optionally pass registers argument to set a number of register calls.`, name: 'register', - type: 'function(name: string)', + type: 'function(name: string, registers: number = 1)', }, { - description: 'Resolves a request for the loading mask referenced by the name parameter.', + description: `Resolves a request for the loading mask referenced by the name parameter. + Can optionally pass resolves argument to set a number of resolve calls.`, name: 'resolve', - type: 'function(name: string)', + type: 'function(name: string, resolves: number = 1)', }, { description: `Creates a fullscreen loading mask and attaches it to the viewContainerRef. Only displayed when the mask has a request registered on it.`, @@ -55,7 +56,9 @@ export class LoadingDemoComponent { type: 'function(options: ILoadingOptions, viewContainerRef: ViewContainerRef)', }]; - constructor(viewContainer: ViewContainerRef, private _loadingService: TdLoadingService) { + constructor(viewContainer: ViewContainerRef, + private _changeDetectorRef: ChangeDetectorRef, + private _loadingService: TdLoadingService) { let options: ILoadingOptions = { name: 'test.overlay', type: LoadingType.Circular, @@ -68,28 +71,29 @@ export class LoadingDemoComponent { this._loadingService.createOverlayComponent(options2, viewContainer); } + ngAfterViewInit(): void { + this.registerLoadingReplace(); + this._changeDetectorRef.detectChanges(); + } + registerCircleLoadingOverlay(): void { this._loadingService.register('test.overlay'); - TimerWrapper.setTimeout( - () => { - this._loadingService.resolve('test.overlay'); - }, - 3000); + setTimeout(() => { + this._loadingService.resolve('test.overlay'); + }, 3000); } registerLinearLoadingOverlay(): void { this._loadingService.register('test.overlay2'); - TimerWrapper.setTimeout( - () => { - this._loadingService.resolve('test.overlay2'); - }, - 3000); + setTimeout(() => { + this._loadingService.resolve('test.overlay2'); + }, 3000); } registerLoadingReplace(): void { - this.replaceRegistered++; this._loadingService.register('test'); this._loadingService.register('test2'); + this.replaceRegistered++; } resolveLoadingReplace(): void { diff --git a/src/platform/core/index.ts b/src/platform/core/index.ts index 222b54d7f7..9f109eaf77 100644 --- a/src/platform/core/index.ts +++ b/src/platform/core/index.ts @@ -43,6 +43,12 @@ export { TdStepComponent, StepState } from './steps/step.component'; export { TdStepsComponent, IStepChangeEvent } from './steps/steps.component'; // Loading +import { TdLoadingComponent } from './loading/loading.component'; + +export const TD_LOADING_ENTRY_COMPONENTS: Type[] = [ + TdLoadingComponent, +]; + export { LoadingType } from './loading/loading.component'; export { TdLoadingService, ILoadingOptions } from './loading/services/loading.service'; export { TdLoadingDirective } from './loading/directives/loading.directive'; diff --git a/src/platform/core/loading/directives/loading.directive.ts b/src/platform/core/loading/directives/loading.directive.ts index 93dcab60f4..547752296a 100644 --- a/src/platform/core/loading/directives/loading.directive.ts +++ b/src/platform/core/loading/directives/loading.directive.ts @@ -1,4 +1,4 @@ -import { Directive, Input, OnInit } from '@angular/core'; +import { Directive, Input, OnInit, OnDestroy } from '@angular/core'; import { ViewContainerRef, TemplateRef } from '@angular/core'; import { LoadingType } from '../loading.component'; @@ -7,7 +7,7 @@ import { TdLoadingService, ILoadingOptions } from '../services/loading.service'; @Directive({ selector: '[tdLoading]', }) -export class TdLoadingDirective implements OnInit { +export class TdLoadingDirective implements OnInit, OnDestroy { private _type: LoadingType; private _name: string; @@ -38,15 +38,19 @@ export class TdLoadingDirective implements OnInit { } } - constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef, - private _loadingService: TdLoadingService) { - this._viewContainer.createEmbeddedView(this._templateRef); - } + constructor(private _viewContainer: ViewContainerRef, + private _templateRef: TemplateRef, + private _loadingService: TdLoadingService) {} ngOnInit(): void { + this._viewContainer.createEmbeddedView(this._templateRef); this._registerComponent(); } + ngOnDestroy(): void { + this._loadingService.removeComponent(this._name); + } + /** * Creates [TdLoadingComponent] and attaches it to this directive's [ViewContainerRef]. * Passes this directive's [TemplateRef] to detach/attach it from DOM when loading mask is on. diff --git a/src/platform/core/loading/loading.component.html b/src/platform/core/loading/loading.component.html index 1d41a38f5d..44c50a1e6d 100644 --- a/src/platform/core/loading/loading.component.html +++ b/src/platform/core/loading/loading.component.html @@ -11,7 +11,8 @@ + [style.height]="getCircleDiameter()" + [style.width]="getCircleDiameter()"> void = () => { + // empty function +}; + export interface ILoadingOptions { name: string; type?: LoadingType; @@ -28,8 +32,9 @@ export class TdLoadingService { private _loadingSources: {[key: string]: Subject} = {}; private _loadingObservables: {[key: string]: Observable} = {}; - constructor(private _componentResolver: ComponentResolver, - private _injector: Injector) { + constructor(private _componentFactoryResolver: ComponentFactoryResolver, + private _injector: Injector, + private _ngZone: NgZone) { } /** @@ -46,24 +51,28 @@ export class TdLoadingService { public createOverlayComponent(options: ILoadingOptions, viewContainerRef: ViewContainerRef): void { (options).height = undefined; (options).overlay = true; - this._createComponent(options) - .then((loadingRef: ILoadingRef) => { - let loading: boolean = false; - loadingRef.observable - .subscribe((registered: number) => { - let instance: TdLoadingComponent = loadingRef.ref.instance; - if (registered > 0 && !loading) { - loading = true; + let loadingRef: ILoadingRef = this._createComponent(options); + let loading: boolean = false; + loadingRef.observable + .subscribe((registered: number) => { + let instance: TdLoadingComponent = loadingRef.ref.instance; + if (registered > 0 && !loading) { + loading = true; + this._ngZone.runOutsideAngular(() => { viewContainerRef.insert(loadingRef.ref.hostView, 0); instance.startInAnimation(); - } else if (registered <= 0 && loading) { - loading = false; + this._ngZone.run(noop); + }); + } else if (registered <= 0 && loading) { + loading = false; + this._ngZone.runOutsideAngular(() => { let subs: Subscription = instance.startOutAnimation().subscribe(() => { subs.unsubscribe(); viewContainerRef.detach(viewContainerRef.indexOf(loadingRef.ref.hostView)); + this._ngZone.run(noop); }); - } - }); + }); + } }); } @@ -84,29 +93,33 @@ export class TdLoadingService { let nativeElement: HTMLElement = templateRef.elementRef.nativeElement; (options).height = nativeElement.nextElementSibling.scrollHeight; (options).overlay = false; - this._createComponent(options) - .then((loadingRef: ILoadingRef) => { - let loading: boolean = false; - loadingRef.observable - .subscribe((registered: number) => { - let instance: TdLoadingComponent = loadingRef.ref.instance; - if (registered > 0 && !loading) { - loading = true; + let loadingRef: ILoadingRef = this._createComponent(options); + let loading: boolean = false; + loadingRef.observable + .subscribe((registered: number) => { + let instance: TdLoadingComponent = loadingRef.ref.instance; + if (registered > 0 && !loading) { + loading = true; + this._ngZone.runOutsideAngular(() => { let index: number = viewContainerRef.indexOf(loadingRef.ref.hostView); if (index < 0) { viewContainerRef.clear(); viewContainerRef.insert(loadingRef.ref.hostView, 0); } instance.startInAnimation(); - } else if (registered <= 0 && loading) { - loading = false; + this._ngZone.run(noop); + }); + } else if (registered <= 0 && loading) { + loading = false; + this._ngZone.runOutsideAngular(() => { let subs: Subscription = instance.startOutAnimation().subscribe(() => { subs.unsubscribe(); viewContainerRef.createEmbeddedView(templateRef); viewContainerRef.detach(viewContainerRef.indexOf(loadingRef.ref.hostView)); + this._ngZone.run(noop); }); - } - }); + }); + } }); } @@ -114,35 +127,65 @@ export class TdLoadingService { * params: * - name: string * + * Removes loading mask from service context. + */ + public removeComponent(name: string): void { + if (this._context[name]) { + this._loadingSources[name] = undefined; + delete this._loadingSources[name]; + this._context[name].loadingRef.destroy(); + this._context[name] = undefined; + delete this._context[name]; + } + } + + /** + * params: + * - name: string + * - registers?: number + * returns: true if successful + * * Resolves a request for the loading mask referenced by the name parameter. + * Can optionally pass registers argument to set a number of register calls. */ - public register(name: string): void { + public register(name: string, registers: number = 1): boolean { if (this._loadingSources[name]) { - this._loadingSources[name].next(++this._context[name].times); + registers = registers < 1 ? 1 : registers; + this._context[name].times += registers; + this._loadingSources[name].next(this._context[name].times); + return true; } + return false; } /** * params: * - name: string + * - resolves?: number + * returns: true if successful * * Registers a request for the loading mask referenced by the name parameter. + * Can optionally pass resolves argument to set a number of resolve calls. */ - public resolve(name: string): void { + public resolve(name: string, resolves: number = 1): boolean { if (this._loadingSources[name]) { - let times: number = 0; + resolves = resolves < 1 ? 1 : resolves; if (this._context[name].times > 0) { - times = --this._context[name].times; + let times: number = this._context[name].times; + times -= resolves; + this._context[name].times = times < 0 ? 0 : times; } - this._loadingSources[name].next(times); + this._loadingSources[name].next(this._context[name].times); + return true; } + return false; } /** * Creates a generic [TdLoadingComponent] and its context. * Returns a promise that resolves to a [ILoadingRef] with the created [ComponentRef] and its referenced [Observable]. */ - private _createComponent(options: IInternalLoadingOptions): Promise { + private _createComponent(options: IInternalLoadingOptions): ILoadingRef { let name: string = options.name; if (!name) { throw 'Name is required for Loading Component.'; @@ -152,19 +195,15 @@ export class TdLoadingService { } else { throw 'Name duplication: Loading Component name conflict.'; } - return new Promise((resolve: Function) => { - this._componentResolver.resolveComponent(TdLoadingComponent) - .then((cf: ComponentFactory) => { - this._context[name].loadingRef = cf.create(this._injector); - this._context[name].times = 0; - this._mapOptions(options, this._context[name].loadingRef.instance); - let compRef: ILoadingRef = { - observable: this._registerLoadingComponent(name), - ref: this._context[name].loadingRef, - }; - resolve(compRef); - }); - }); + this._context[name].loadingRef = this._componentFactoryResolver + .resolveComponentFactory(TdLoadingComponent).create(this._injector); + this._context[name].times = 0; + this._mapOptions(options, this._context[name].loadingRef.instance); + let compRef: ILoadingRef = { + observable: this._registerLoadingComponent(name), + ref: this._context[name].loadingRef, + }; + return compRef; } /**