-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components): new component loader provided
PS: added popovers - demo page required
- Loading branch information
Showing
28 changed files
with
1,771 additions
and
252 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
// todo: add delay support | ||
// todo: merge events onShow, onShown, etc... | ||
// todo: add global positioning configuration? | ||
import { | ||
NgZone, ViewContainerRef, ComponentFactoryResolver, Injector, Renderer, | ||
ElementRef, ComponentRef, ComponentFactory, Type, TemplateRef, EventEmitter | ||
} from '@angular/core'; | ||
import { ContentRef } from './content-ref.class'; | ||
import { PositioningService, PositioningOptions } from '../positioning'; | ||
import { listenToTriggers } from '../utils/triggers'; | ||
|
||
export interface ListenOptions { | ||
target?: ElementRef; | ||
triggers?: string; | ||
show?: Function; | ||
hide?: Function; | ||
toggle?: Function; | ||
} | ||
|
||
export class ComponentLoader<T> { | ||
public onBeforeShow: EventEmitter<any> = new EventEmitter(); | ||
public onShown: EventEmitter<any> = new EventEmitter(); | ||
public onBeforeHide: EventEmitter<any> = new EventEmitter(); | ||
public onHidden: EventEmitter<any> = new EventEmitter(); | ||
|
||
public instance: T; | ||
|
||
private _componentFactory: ComponentFactory<T>; | ||
private _elementRef: ElementRef; | ||
private _componentRef: ComponentRef<T>; | ||
private _zoneSubscription: any; | ||
private _contentRef: ContentRef; | ||
private _viewContainerRef: ViewContainerRef; | ||
private _injector: Injector; | ||
private _renderer: Renderer; | ||
private _ngZone: NgZone; | ||
private _componentFactoryResolver: ComponentFactoryResolver; | ||
private _posService: PositioningService; | ||
|
||
private _unregisterListenersFn: Function; | ||
|
||
public get isShown(): boolean { | ||
return !!this._componentRef; | ||
}; | ||
|
||
/** | ||
* Placement of a component. Accepts: "top", "bottom", "left", "right" | ||
*/ | ||
private attachment: string; | ||
|
||
/** | ||
* A selector specifying the element the popover should be appended to. | ||
* Currently only supports "body". | ||
*/ | ||
private container: string | ElementRef | any; | ||
|
||
/** | ||
* Specifies events that should trigger. Supports a space separated list of | ||
* event names. | ||
*/ | ||
private triggers: string; | ||
|
||
/** | ||
* Do not use this directly, it should be instanced via | ||
* `ComponentLoadFactory.attach` | ||
* @internal | ||
* @param _viewContainerRef | ||
* @param _elementRef | ||
* @param _injector | ||
* @param _renderer | ||
* @param _componentFactoryResolver | ||
* @param _ngZone | ||
* @param _posService | ||
*/ | ||
// tslint:disable-next-line | ||
public constructor(_viewContainerRef: ViewContainerRef, _renderer: Renderer, | ||
_elementRef: ElementRef, | ||
_injector: Injector, _componentFactoryResolver: ComponentFactoryResolver, | ||
_ngZone: NgZone, _posService: PositioningService) { | ||
this._ngZone = _ngZone; | ||
this._injector = _injector; | ||
this._renderer = _renderer; | ||
this._elementRef = _elementRef; | ||
this._posService = _posService; | ||
this._viewContainerRef = _viewContainerRef; | ||
this._componentFactoryResolver = _componentFactoryResolver; | ||
} | ||
|
||
public attach(compType: Type<T>): ComponentLoader<T> { | ||
this._componentFactory = this._componentFactoryResolver | ||
.resolveComponentFactory<T>(compType); | ||
return this; | ||
} | ||
|
||
// todo: add behaviour: to target element, `body`, custom element | ||
public to(container?: string): ComponentLoader<T> { | ||
this.container = container || this.container; | ||
return this; | ||
} | ||
|
||
public position(opts?: PositioningOptions): ComponentLoader<T> { | ||
this.attachment = opts.attachment || this.attachment; | ||
this._elementRef = opts.target as ElementRef || this._elementRef; | ||
return this; | ||
} | ||
|
||
public show(content?: string | TemplateRef<any>, mixin?: any): ComponentRef<T> { | ||
this._subscribePositioning(); | ||
|
||
if (!this._componentRef) { | ||
this.onBeforeShow.emit(); | ||
this._contentRef = this._getContentRef(content); | ||
this._componentRef = this._viewContainerRef | ||
.createComponent(this._componentFactory, 0, this._injector, this._contentRef.nodes); | ||
this.instance = this._componentRef.instance; | ||
|
||
Object.assign(this._componentRef.instance, mixin || {}); | ||
|
||
if (this.container === 'body' && typeof document !== 'undefined') { | ||
document.querySelector(this.container as string) | ||
.appendChild(this._componentRef.location.nativeElement); | ||
} | ||
|
||
// we need to manually invoke change detection since events registered | ||
// via | ||
// Renderer::listen() are not picked up by change detection with the | ||
// OnPush strategy | ||
this._componentRef.changeDetectorRef.markForCheck(); | ||
this.onShown.emit(this._componentRef.instance); | ||
} | ||
return this._componentRef; | ||
} | ||
|
||
public hide(): ComponentLoader<T> { | ||
if (this._componentRef) { | ||
this.onBeforeHide.emit(this._componentRef.instance); | ||
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._componentRef.hostView)); | ||
this._componentRef = null; | ||
|
||
if (this._contentRef.viewRef) { | ||
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._contentRef.viewRef)); | ||
this._contentRef = null; | ||
} | ||
|
||
this._componentRef = null; | ||
this.onHidden.emit(); | ||
} | ||
return this; | ||
} | ||
|
||
public toggle(): void { | ||
if (this.isShown) { | ||
this.hide(); | ||
return; | ||
} | ||
|
||
this.show(); | ||
} | ||
|
||
public dispose(): void { | ||
if (this.isShown) { | ||
this.hide(); | ||
} | ||
|
||
this._unsubscribePositioning(); | ||
|
||
if (this._unregisterListenersFn) { | ||
this._unregisterListenersFn(); | ||
} | ||
} | ||
|
||
public listen(listenOpts: ListenOptions): ComponentLoader<T> { | ||
if (this._unregisterListenersFn) { | ||
this._unregisterListenersFn(); | ||
} | ||
|
||
this.triggers = listenOpts.triggers || this.triggers; | ||
|
||
listenOpts.target = listenOpts.target || this._elementRef; | ||
listenOpts.show = listenOpts.show || (() => this.show()); | ||
listenOpts.hide = listenOpts.hide || (() => this.hide()); | ||
listenOpts.toggle = listenOpts.toggle || (() => this.isShown | ||
? listenOpts.hide() | ||
: listenOpts.show()); | ||
|
||
this._unregisterListenersFn = listenToTriggers( | ||
this._renderer, | ||
listenOpts.target.nativeElement, | ||
this.triggers, | ||
listenOpts.show, | ||
listenOpts.hide, | ||
listenOpts.toggle); | ||
|
||
return this; | ||
} | ||
|
||
private _subscribePositioning(): void { | ||
if (this._zoneSubscription) { | ||
return; | ||
} | ||
|
||
this._zoneSubscription = this._ngZone | ||
.onStable.subscribe(() => { | ||
if (!this._componentRef) { | ||
return; | ||
} | ||
this._posService.position({ | ||
element: this._componentRef.location, | ||
target: this._elementRef, | ||
attachment: this.attachment, | ||
appendToBody: this.container === 'body' | ||
}); | ||
}); | ||
} | ||
|
||
private _unsubscribePositioning(): void { | ||
if (!this._zoneSubscription) { | ||
return; | ||
} | ||
this._zoneSubscription.unsubscribe(); | ||
this._zoneSubscription = null; | ||
} | ||
|
||
private _getContentRef(content: string | TemplateRef<any>): ContentRef { | ||
if (!content) { | ||
return new ContentRef([]); | ||
} | ||
|
||
if (content instanceof TemplateRef) { | ||
const viewRef = this._viewContainerRef | ||
.createEmbeddedView<TemplateRef<T>>(content); | ||
return new ContentRef([viewRef.rootNodes], viewRef); | ||
} | ||
|
||
return new ContentRef([[this._renderer.createText(null, `${content}`)]]); | ||
} | ||
} |
Oops, something went wrong.