Skip to content

Commit ebcba9f

Browse files
committed
refactor(popover): use ComponentRef setInput() api, input signals, host bindings
1 parent 30e289b commit ebcba9f

File tree

4 files changed

+150
-148
lines changed

4 files changed

+150
-148
lines changed

projects/coreui-angular/src/lib/popover/popover.directive.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,25 @@ import { IntersectionService, ListenersService } from '../services';
44
import { PopoverDirective } from './popover.directive';
55

66
describe('PopoverDirective', () => {
7-
let document: Document;
87
let renderer: Renderer2;
98
let hostElement: ElementRef;
109
let viewContainerRef: ViewContainerRef;
1110
let changeDetectorRef: ChangeDetectorRef;
1211

1312
it('should create an instance', () => {
1413
TestBed.configureTestingModule({
15-
providers: [IntersectionService, Renderer2, ListenersService],
14+
providers: [IntersectionService, Renderer2, ListenersService]
1615
});
1716
const intersectionService = TestBed.inject(IntersectionService);
1817
const listenersService = TestBed.inject(ListenersService);
1918
TestBed.runInInjectionContext(() => {
2019
const directive = new PopoverDirective(
21-
document,
2220
renderer,
2321
hostElement,
2422
viewContainerRef,
2523
listenersService,
2624
changeDetectorRef,
27-
intersectionService,
25+
intersectionService
2826
);
2927
expect(directive).toBeTruthy();
3028
});

projects/coreui-angular/src/lib/popover/popover.directive.ts

Lines changed: 111 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,108 @@
1+
import { DOCUMENT } from '@angular/common';
12
import {
23
AfterViewInit,
34
ChangeDetectorRef,
45
ComponentRef,
6+
computed,
57
DestroyRef,
68
Directive,
9+
effect,
710
ElementRef,
8-
HostBinding,
911
inject,
10-
Inject,
11-
Input,
12-
OnChanges,
12+
input,
13+
model,
1314
OnDestroy,
1415
OnInit,
1516
Renderer2,
16-
SimpleChanges,
1717
TemplateRef,
1818
ViewContainerRef
1919
} from '@angular/core';
2020
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
21-
import { DOCUMENT } from '@angular/common';
2221
import { debounceTime, filter, finalize } from 'rxjs/operators';
2322
import { createPopper, Instance, Options } from '@popperjs/core';
2423

2524
import { Triggers } from '../coreui.types';
25+
import { IListenersConfig, IntersectionService, ListenersService } from '../services';
26+
import { ElementRefDirective } from '../shared';
2627
import { PopoverComponent } from './popover/popover.component';
27-
import { IListenersConfig, ListenersService } from '../services/listeners.service';
28-
import { IntersectionService } from '../services';
2928

3029
@Directive({
3130
selector: '[cPopover]',
3231
exportAs: 'cPopover',
33-
providers: [ListenersService],
34-
standalone: true
32+
providers: [ListenersService, IntersectionService],
33+
standalone: true,
34+
host: { '[attr.aria-describedby]': 'ariaDescribedBy' }
3535
})
36-
export class PopoverDirective implements OnChanges, OnDestroy, OnInit, AfterViewInit {
37-
36+
export class PopoverDirective implements OnDestroy, OnInit, AfterViewInit {
3837
/**
3938
* Content of popover
4039
* @type {string | TemplateRef}
4140
*/
42-
@Input('cPopover') content: string | TemplateRef<any> = '';
41+
readonly content = input<string | TemplateRef<any> | undefined>(undefined, { alias: 'cPopover' });
42+
43+
contentEffect = effect(() => {
44+
if (this.content()) {
45+
this.destroyTooltipElement();
46+
}
47+
});
4348

4449
/**
4550
* Optional popper Options object, takes precedence over cPopoverPlacement prop
4651
* @type Partial<Options>
4752
*/
48-
@Input('cPopoverOptions')
49-
set popperOptions(value: Partial<Options>) {
50-
this._popperOptions = { ...this._popperOptions, placement: this.placement, ...value };
51-
};
53+
readonly popperOptions = input<Partial<Options>>({}, { alias: 'cPopoverOptions' });
5254

53-
get popperOptions(): Partial<Options> {
54-
return { placement: this.placement, ...this._popperOptions };
55-
}
55+
popperOptionsEffect = effect(() => {
56+
this._popperOptions = {
57+
...this._popperOptions,
58+
placement: this.placement(),
59+
...this.popperOptions()
60+
};
61+
});
62+
63+
popperOptionsComputed = computed(() => {
64+
return { placement: this.placement(), ...this._popperOptions };
65+
});
5666

5767
/**
5868
* Describes the placement of your component after Popper.js has applied all the modifiers that may have flipped or altered the originally provided placement property.
69+
* @type: 'top' | 'bottom' | 'left' | 'right'
70+
* @default: 'top'
5971
*/
60-
@Input('cPopoverPlacement') placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
72+
readonly placement = input<'top' | 'bottom' | 'left' | 'right'>('top', { alias: 'cPopoverPlacement' });
73+
74+
/**
75+
* ElementRefDirective for positioning the tooltip on reference element
76+
* @type: ElementRefDirective
77+
* @default: undefined
78+
*/
79+
readonly reference = input<ElementRefDirective | undefined>(undefined, { alias: 'cTooltipRef' });
80+
81+
readonly referenceRef = computed(() => this.reference()?.elementRef ?? this.hostElement);
82+
6183
/**
6284
* Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them.
63-
* @type {'hover' | 'focus' | 'click'}
85+
* @type: 'Triggers | Triggers[]
6486
*/
65-
@Input('cPopoverTrigger') trigger?: Triggers | Triggers[] = 'hover';
87+
readonly trigger = input<Triggers | Triggers[]>('hover', { alias: 'cPopoverTrigger' });
6688

6789
/**
6890
* Toggle the visibility of popover component.
91+
* @type boolean
6992
*/
70-
@Input('cPopoverVisible')
71-
set visible(value: boolean) {
72-
this._visible = value;
73-
}
74-
75-
get visible() {
76-
return this._visible;
77-
}
93+
readonly visible = model(false, { alias: 'cPopoverVisible' });
7894

79-
private _visible = false;
95+
visibleEffect = effect(() => {
96+
this.visible() ? this.addTooltipElement() : this.removeTooltipElement();
97+
});
8098

81-
@HostBinding('attr.aria-describedby') get ariaDescribedBy(): string | null {
82-
return this.popoverId ? this.popoverId : null;
99+
get ariaDescribedBy(): string | null {
100+
return this.tooltipId ? this.tooltipId : null;
83101
}
84102

85-
private popover!: HTMLDivElement;
86-
private popoverId!: string;
87-
private popoverRef!: ComponentRef<PopoverComponent>;
103+
private tooltip!: HTMLDivElement;
104+
private tooltipId!: string;
105+
private tooltipRef!: ComponentRef<PopoverComponent>;
88106
private popperInstance!: Instance;
89107

90108
private _popperOptions: Partial<Options> = {
@@ -99,9 +117,9 @@ export class PopoverDirective implements OnChanges, OnDestroy, OnInit, AfterView
99117
};
100118

101119
readonly #destroyRef = inject(DestroyRef);
120+
readonly #document = inject(DOCUMENT);
102121

103122
constructor(
104-
@Inject(DOCUMENT) private document: Document,
105123
private renderer: Renderer2,
106124
private hostElement: ElementRef,
107125
private viewContainerRef: ViewContainerRef,
@@ -114,15 +132,9 @@ export class PopoverDirective implements OnChanges, OnDestroy, OnInit, AfterView
114132
this.intersectionServiceSubscribe();
115133
}
116134

117-
ngOnChanges(changes: SimpleChanges): void {
118-
if (changes['visible']) {
119-
changes['visible'].currentValue ? this.addPopoverElement() : this.removePopoverElement();
120-
}
121-
}
122-
123135
ngOnDestroy(): void {
124136
this.clearListeners();
125-
this.destroyPopoverElement();
137+
this.destroyTooltipElement();
126138
}
127139

128140
ngOnInit(): void {
@@ -132,18 +144,15 @@ export class PopoverDirective implements OnChanges, OnDestroy, OnInit, AfterView
132144
private setListeners(): void {
133145
const config: IListenersConfig = {
134146
hostElement: this.hostElement,
135-
trigger: this.trigger,
147+
trigger: this.trigger(),
136148
callbackToggle: () => {
137-
this.visible = !this.visible;
138-
this.visible ? this.addPopoverElement() : this.removePopoverElement();
149+
this.visible.set(!this.visible());
139150
},
140151
callbackOff: () => {
141-
this.visible = false;
142-
this.removePopoverElement();
152+
this.visible.set(false);
143153
},
144154
callbackOn: () => {
145-
this.visible = true;
146-
this.addPopoverElement();
155+
this.visible.set(true);
147156
}
148157
};
149158
this.listenersService.setListeners(config);
@@ -154,93 +163,93 @@ export class PopoverDirective implements OnChanges, OnDestroy, OnInit, AfterView
154163
}
155164

156165
private intersectionServiceSubscribe(): void {
157-
this.intersectionService.createIntersectionObserver(this.hostElement);
166+
this.intersectionService.createIntersectionObserver(this.referenceRef());
158167
this.intersectionService.intersecting$
159168
.pipe(
160-
filter(next => next.hostElement === this.hostElement),
169+
filter((next) => next.hostElement === this.referenceRef()),
161170
debounceTime(100),
162171
finalize(() => {
163-
this.intersectionService.unobserve(this.hostElement);
172+
this.intersectionService.unobserve(this.referenceRef());
164173
}),
165174
takeUntilDestroyed(this.#destroyRef)
166175
)
167-
.subscribe(next => {
168-
this.visible = next.isIntersecting ? this.visible : false;
169-
!this.visible && this.removePopoverElement();
176+
.subscribe((next) => {
177+
this.visible.set(next.isIntersecting ? this.visible() : false);
170178
});
171179
}
172180

173181
private getUID(prefix: string): string {
174182
let uid = prefix ?? 'random-id';
175183
do {
176184
uid = `${prefix}-${Math.floor(Math.random() * 1000000).toString(10)}`;
177-
} while (this.document.getElementById(uid));
185+
} while (this.#document.getElementById(uid));
178186

179187
return uid;
180188
}
181189

182-
private createPopoverElement(): void {
183-
if (!this.popoverRef) {
184-
this.popoverRef = this.viewContainerRef.createComponent<PopoverComponent>(PopoverComponent);
190+
private createTooltipElement(): void {
191+
if (!this.tooltipRef) {
192+
this.tooltipRef = this.viewContainerRef.createComponent<PopoverComponent>(PopoverComponent);
185193
// this.viewContainerRef.detach();
186194
}
187195
}
188196

189-
private destroyPopoverElement(): void {
190-
this.popover?.remove();
191-
this.popoverRef?.destroy();
197+
private destroyTooltipElement(): void {
198+
this.tooltip?.remove();
199+
this.tooltipRef?.destroy();
192200
// @ts-ignore
193-
this.popoverRef = undefined;
201+
this.tooltipRef = undefined;
194202
this.popperInstance?.destroy();
195203
this.viewContainerRef?.detach();
196204
this.viewContainerRef?.clear();
197205
}
198206

199-
private addPopoverElement(): void {
200-
if (!this.popoverRef) {
201-
this.createPopoverElement();
207+
private addTooltipElement(): void {
208+
if (!this.content()) {
209+
this.destroyTooltipElement();
210+
return;
211+
}
212+
213+
if (!this.tooltipRef) {
214+
this.createTooltipElement();
202215
}
203-
this.popoverRef.instance.content = this.content;
204-
this.popover = this.popoverRef.location.nativeElement;
205-
this.renderer.addClass(this.popover, 'd-none');
206-
this.renderer.addClass(this.popover, 'fade');
216+
217+
this.tooltipRef?.setInput('content', this.content() ?? '');
218+
219+
this.tooltip = this.tooltipRef?.location.nativeElement;
220+
this.renderer.addClass(this.tooltip, 'd-none');
221+
this.renderer.addClass(this.tooltip, 'fade');
207222

208223
this.popperInstance?.destroy();
209224

210-
setTimeout(() => {
211-
this.popperInstance = createPopper(
212-
this.hostElement.nativeElement,
213-
this.popover,
214-
{ ...this.popperOptions }
215-
);
216-
this.viewContainerRef.insert(this.popoverRef.hostView);
217-
this.renderer.appendChild(this.document.body, this.popover);
218-
if (!this.visible) {
219-
this.removePopoverElement();
220-
return;
221-
}
222-
setTimeout(() => {
223-
this.popoverId = this.getUID('popover');
224-
this.popoverRef.instance.id = this.popoverId;
225-
if (!this.visible) {
226-
this.removePopoverElement();
227-
return;
228-
}
229-
this.renderer.removeClass(this.popover, 'd-none');
230-
this.popoverRef.instance.visible = this.visible;
231-
this.popperInstance.forceUpdate();
232-
this.changeDetectorRef.markForCheck();
233-
}, 100);
225+
this.viewContainerRef.insert(this.tooltipRef.hostView);
226+
this.renderer.appendChild(this.#document.body, this.tooltip);
227+
228+
this.popperInstance = createPopper(this.referenceRef().nativeElement, this.tooltip, {
229+
...this.popperOptionsComputed()
234230
});
231+
232+
if (!this.visible()) {
233+
this.removeTooltipElement();
234+
return;
235+
}
236+
setTimeout(() => {
237+
this.tooltipId = this.getUID('popover');
238+
this.tooltipRef?.setInput('id', this.tooltipId);
239+
this.renderer.removeClass(this.tooltip, 'd-none');
240+
this.tooltipRef?.setInput('visible', this.visible());
241+
this.popperInstance?.forceUpdate();
242+
this.changeDetectorRef?.markForCheck();
243+
}, 100);
235244
}
236245

237-
private removePopoverElement(): void {
238-
this.popoverId = '';
239-
if (!this.popoverRef) {
246+
private removeTooltipElement(): void {
247+
this.tooltipId = '';
248+
if (!this.tooltipRef) {
240249
return;
241250
}
242-
this.popoverRef.instance.visible = false;
243-
this.popoverRef.instance.id = undefined;
251+
this.tooltipRef.setInput('visible', false);
252+
this.tooltipRef.setInput('id', undefined);
244253
this.changeDetectorRef.markForCheck();
245254
setTimeout(() => {
246255
this.viewContainerRef?.detach();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<ng-container>
2-
<div [ngClass]="{'popover-arrow': !!content}" data-popper-arrow></div>
2+
<div [ngClass]="{'popover-arrow': !!content()}" data-popper-arrow></div>
33
<ng-container #popoverTemplate />
44
</ng-container>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy