Skip to content

Commit

Permalink
feat(components): new component loader provided
Browse files Browse the repository at this point in the history
PS: added popovers - demo page required
  • Loading branch information
valorkin committed Dec 11, 2016
1 parent 8daa4be commit 3e53b7d
Show file tree
Hide file tree
Showing 28 changed files with 1,771 additions and 252 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<div accordion-heading>
I can have markup, too!
<i class="pull-right glyphicon"
[ngClass]="{'glyphicon-chevron-down': group?.isOpen, 'glyphicon-chevron-right': !group?.isOpen}"></i>
[ngClass]="{'glyphicon-chevron-down': group?._isOpen, 'glyphicon-chevron-right': !group?._isOpen}"></i>
</div>
This is just some content to illustrate fancy headings.
</accordion-group>
Expand Down
85 changes: 55 additions & 30 deletions demo/src/app/components/tooltip/demos/tooltip-demo.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,72 +10,97 @@
Pellentesque <a href="#" [tooltip]="dynamicTooltip">{{dynamicTooltipText}}</a>,
sit amet venenatis urna cursus eget nunc scelerisque viverra mauris, in
aliquam. Tincidunt lobortis feugiat vivamus at
<a href="#" tooltipPlacement="left" tooltip="On the Left!">left</a> eget
<a href="#" placement="left" tooltip="On the Left!">left</a> eget
arcu dictum varius duis at consectetur lorem. Vitae elementum curabitur
<a href="#" tooltipPlacement="right" tooltip="On the Right!">right</a>
<a href="#" placement="right" tooltip="On the Right!">right</a>
nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
<a href="#" tooltipPlacement="bottom" tooltip="On the Bottom!">bottom</a>
<a href="#" placement="bottom" tooltip="On the Bottom!">bottom</a>
pharetra convallis posuere morbi leo urna,
<a href="#" [tooltipAnimation]="false" tooltip="I don't fade. :-(">fading</a>
at elementum eu, facilisis sed odio morbi quis commodo odio. In cursus
<a href="#" tooltipPopupDelay='1000' tooltip='appears with delay'>delayed</a> turpis massa tincidunt dui ut.
<a href="#" tooltipPopupDelay='1000' tooltip='appears with delay'>delayed</a>
turpis massa tincidunt dui ut.
nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
</p>

<p>
I can even contain HTML. <a href="#" [tooltipPopupDelay]="500" [tooltipHtml]="htmlTooltip" (tooltipStateChanged)="tooltipStateChanged($event)">Check me out!</a>
I can even contain HTML.
<a href="#" [tooltip]="tipContent">Check me out!</a>
<template #tipContent>Say, <b>{{dynamicTooltipText}}</b>!</template>
</p>

<template #toolTipTemplate let-model="model">
<template #toolTipTemplate>
<h4>Tool tip custom content defined inside a template</h4>
<h5>With context binding: {{model.text}}</h5>
<h5>With context binding: {{dynamicTooltip}}</h5>
</template>

<p>
Or use a TemplateRef. <a href="#" [tooltipHtml]="toolTipTemplate"
[tooltipContext]="tooltipModel"
(tooltipStateChanged)="tooltipStateChanged($event)">Check me out!</a>
Or use a TemplateRef.
<a href="#" [tooltip]="toolTipTemplate">Check me out!</a>
</p>
<p>
Programatically show/hide tooltip
<a href="#" [tooltip]="'Foo'"
(tooltipStateChanged)="tooltipStateChanged($event)"
#tooltip="bs-tooltip">Check me out!</a>
<button class="btn btn-primary" (click)="tooltip.show()">Show tooltip</button>
<button class="btn btn-danger" (click)="tooltip.hide()">Hide tooltip</button>
</p>

<p>
I can have a custom class. <a href="#" [tooltip]="'I can have a custom class applied to me!'" [tooltipClass]="'customClass'" [tooltipFadeDuration]="1000">Check me out!</a>
<a href="#" tooltip="Foo"
(onShown)="tooltipStateChanged($event)"
(onHidden)="tooltipStateChanged($event)"
#tooltip="bs-tooltip">Check me out!</a>
<button class="btn btn-primary" (click)="tooltip.show()">Show tooltip</button>
<button class="btn btn-danger" (click)="tooltip.hide()">Hide tooltip</button>
</p>

<!--<p>-->
<!--I can have a custom class.-->
<!--<a href="#"-->
<!--[tooltip]="'I can have a custom class applied to me!'"-->
<!--[tooltipClass]="'customClass'"-->
<!--[tooltipFadeDuration]="1000">Check me out!</a>-->
<!--</p>-->

<p>
I can triggered by the custom events. For example, by the click. <a href="#" tooltip="I displayed after click event" tooltipTrigger="click">Check me out</a>
I can triggered by the custom events. For example, by the click.
<a href="#"
onclick="return false"
tooltip="I displayed after click event"
triggers="click">Check
me out</a>
</p>

<p>
I can combine trigger events. Now I can be displayed by the "click" and "focus" events.
<a href="#" tooltip="I displayed after click or focus event" [tooltipTrigger]="['focusin', 'click']">Click or tab me.</a>
I can combine trigger events. Now I can be displayed by the "click" and
"focus" events.
<a href="#" tooltip="I displayed after click or focus event"
onclick="return false"
triggers="focus click">Click or tab me.</a>
</p>


<p style="overflow:hidden; position:relative; background-color: #f6f6f6" class="alert">
And if I am in <a href="#" tooltip="That ruins the tooltip">overflow: hidden</a> container, then just <a href="#" tooltip="So the tooltip is visible always correctly" [tooltipAppendToBody]="true">tooltipAppendToBody</a> me instead!
<p style="overflow:hidden; position:relative; background-color: #f6f6f6"
class="alert">
And if I am in <a href="#" tooltip="That ruins the tooltip">overflow:
hidden</a> container, then just
<a href="#"
tooltip="So the tooltip is visible always correctly"
container="body">tooltipAppendToBody</a>
me instead!
</p>

<form role="form">
<div class="form-group">
<label>Or use custom triggers, like focus: </label>
<input type="text" name="clickMe" value="Click me!" tooltip="See? Now click away..." tooltipTrigger="focus" tooltipPlacement="right" class="form-control" />
<input type="text" name="clickMe" value="Click me!"
tooltip="See? Now click away..."
triggers="focus"
placement="bottom" class="form-control"/>
</div>

<div class="form-group" ngClass="{'has-error' : !inputModel}">
<label>Disable tooltips conditionally:</label>
<input type="text" name="inputModel" [(ngModel)]="inputModel" class="form-control"
<input type="text" name="inputModel"
[(ngModel)]="inputModel"
class="form-control"
placeholder="Hover over this for a tooltip until this is filled"
tooltip="Enter something in this input field to disable this tooltip"
tooltipPlacement="top"
tooltipTrigger="mouseenter"
[tooltipEnable]="!inputModel || inputModel.length==0" />
placement="top"
triggers="hover"
[isDisabled]="inputModel"/>
</div>
</form>
2 changes: 1 addition & 1 deletion demo/src/app/components/tooltip/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { TooltipModule } from 'ng2-bootstrap';
CommonModule,
FormsModule,
SharedModule,
TooltipModule
TooltipModule.forRoot()
],
exports: [TooltipSectionComponent]
})
Expand Down
237 changes: 237 additions & 0 deletions src/component-loader/component-loader.class.ts
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}`)]]);
}
}
Loading

0 comments on commit 3e53b7d

Please sign in to comment.