Skip to content

Commit 1243e44

Browse files
committed
refactor(tooltip): use ComponentRef setInput() api, input signals, host bindings
1 parent ebcba9f commit 1243e44

File tree

3 files changed

+63
-76
lines changed

3 files changed

+63
-76
lines changed

projects/coreui-angular/src/lib/tooltip/tooltip.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 { TooltipDirective } from './tooltip.directive';
55

66
describe('TooltipDirective', () => {
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 TooltipDirective(
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/tooltip/tooltip.directive.ts

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DOCUMENT } from '@angular/common';
12
import {
23
AfterViewInit,
34
ChangeDetectorRef,
@@ -7,32 +8,30 @@ import {
78
Directive,
89
effect,
910
ElementRef,
10-
HostBinding,
1111
inject,
12-
Inject,
1312
input,
1413
model,
1514
OnDestroy,
1615
OnInit,
1716
Renderer2,
1817
TemplateRef,
19-
ViewContainerRef,
18+
ViewContainerRef
2019
} from '@angular/core';
21-
import { DOCUMENT } from '@angular/common';
20+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
21+
import { debounceTime, filter, finalize } from 'rxjs/operators';
2222
import { createPopper, Instance, Options } from '@popperjs/core';
2323

2424
import { Triggers } from '../coreui.types';
25-
import { TooltipComponent } from './tooltip/tooltip.component';
2625
import { IListenersConfig, IntersectionService, ListenersService } from '../services';
27-
import { debounceTime, filter, finalize } from 'rxjs/operators';
28-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2926
import { ElementRefDirective } from '../shared';
27+
import { TooltipComponent } from './tooltip/tooltip.component';
3028

3129
@Directive({
3230
selector: '[cTooltip]',
3331
exportAs: 'cTooltip',
3432
providers: [ListenersService, IntersectionService],
3533
standalone: true,
34+
host: { '[attr.aria-describedby]': 'ariaDescribedBy' }
3635
})
3736
export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
3837
/**
@@ -57,7 +56,7 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
5756
this._popperOptions = {
5857
...this._popperOptions,
5958
placement: this.placement(),
60-
...this.popperOptions(),
59+
...this.popperOptions()
6160
};
6261
});
6362

@@ -70,18 +69,14 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
7069
* @type: 'top' | 'bottom' | 'left' | 'right'
7170
* @default: 'top'
7271
*/
73-
readonly placement = input<'top' | 'bottom' | 'left' | 'right'>('top', {
74-
alias: 'cTooltipPlacement',
75-
});
72+
readonly placement = input<'top' | 'bottom' | 'left' | 'right'>('top', { alias: 'cTooltipPlacement' });
7673

7774
/**
7875
* ElementRefDirective for positioning the tooltip on reference element
7976
* @type: ElementRefDirective
8077
* @default: undefined
8178
*/
82-
readonly reference = input<ElementRefDirective | undefined>(undefined, {
83-
alias: 'cTooltipRef',
84-
});
79+
readonly reference = input<ElementRefDirective | undefined>(undefined, { alias: 'cTooltipRef' });
8580

8681
readonly referenceRef = computed(() => this.reference()?.elementRef ?? this.hostElement);
8782

@@ -101,7 +96,7 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
10196
this.visible() ? this.addTooltipElement() : this.removeTooltipElement();
10297
});
10398

104-
@HostBinding('attr.aria-describedby') get ariaDescribedBy(): string | null {
99+
get ariaDescribedBy(): string | null {
105100
return this.tooltipId ? this.tooltipId : null;
106101
}
107102

@@ -115,22 +110,22 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
115110
{
116111
name: 'offset',
117112
options: {
118-
offset: [0, 5],
119-
},
120-
},
121-
],
113+
offset: [0, 5]
114+
}
115+
}
116+
]
122117
};
123118

124119
readonly #destroyRef = inject(DestroyRef);
120+
readonly #document = inject(DOCUMENT);
125121

126122
constructor(
127-
@Inject(DOCUMENT) private document: Document,
128123
private renderer: Renderer2,
129124
private hostElement: ElementRef,
130125
private viewContainerRef: ViewContainerRef,
131126
private listenersService: ListenersService,
132127
private changeDetectorRef: ChangeDetectorRef,
133-
private intersectionService: IntersectionService,
128+
private intersectionService: IntersectionService
134129
) {}
135130

136131
ngAfterViewInit(): void {
@@ -158,7 +153,7 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
158153
},
159154
callbackOn: () => {
160155
this.visible.set(true);
161-
},
156+
}
162157
};
163158
this.listenersService.setListeners(config);
164159
}
@@ -176,7 +171,7 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
176171
finalize(() => {
177172
this.intersectionService.unobserve(this.referenceRef());
178173
}),
179-
takeUntilDestroyed(this.#destroyRef),
174+
takeUntilDestroyed(this.#destroyRef)
180175
)
181176
.subscribe((next) => {
182177
this.visible.set(next.isIntersecting ? this.visible() : false);
@@ -187,7 +182,7 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
187182
let uid = prefix ?? 'random-id';
188183
do {
189184
uid = `${prefix}-${Math.floor(Math.random() * 1000000).toString(10)}`;
190-
} while (this.document.getElementById(uid));
185+
} while (this.#document.getElementById(uid));
191186

192187
return uid;
193188
}
@@ -219,31 +214,30 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
219214
this.createTooltipElement();
220215
}
221216

222-
this.tooltipId = this.getUID('tooltip');
223-
this.tooltipRef.instance.id = this.tooltipId;
224-
this.tooltipRef.instance.content = this.content() ?? '';
217+
this.tooltipRef?.setInput('content', this.content() ?? '');
225218

226-
this.tooltip = this.tooltipRef.location.nativeElement;
219+
this.tooltip = this.tooltipRef?.location.nativeElement;
227220
this.renderer.addClass(this.tooltip, 'd-none');
228221
this.renderer.addClass(this.tooltip, 'fade');
229222

230223
this.popperInstance?.destroy();
231224

232225
this.viewContainerRef.insert(this.tooltipRef.hostView);
233-
this.renderer.appendChild(this.document.body, this.tooltip);
226+
this.renderer.appendChild(this.#document.body, this.tooltip);
234227

235228
this.popperInstance = createPopper(this.referenceRef().nativeElement, this.tooltip, {
236-
...this.popperOptionsComputed(),
229+
...this.popperOptionsComputed()
237230
});
231+
238232
if (!this.visible()) {
239233
this.removeTooltipElement();
240234
return;
241235
}
242-
this.renderer.removeClass(this.tooltip, 'd-none');
243-
this.changeDetectorRef.markForCheck();
244-
245236
setTimeout(() => {
246-
this.tooltipRef && (this.tooltipRef.instance.visible = this.visible());
237+
this.tooltipId = this.getUID('tooltip');
238+
this.tooltipRef?.setInput('id', this.tooltipId);
239+
this.renderer.removeClass(this.tooltip, 'd-none');
240+
this.tooltipRef?.setInput('visible', this.visible());
247241
this.popperInstance?.forceUpdate();
248242
this.changeDetectorRef?.markForCheck();
249243
}, 100);
@@ -254,8 +248,8 @@ export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
254248
if (!this.tooltipRef) {
255249
return;
256250
}
257-
this.tooltipRef.instance.visible = false;
258-
this.tooltipRef.instance.id = undefined;
251+
this.tooltipRef.setInput('visible', false);
252+
this.tooltipRef.setInput('id', undefined);
259253
this.changeDetectorRef.markForCheck();
260254
setTimeout(() => {
261255
this.viewContainerRef?.detach();

projects/coreui-angular/src/lib/tooltip/tooltip/tooltip.component.ts

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,67 @@
11
import {
2-
AfterViewInit,
2+
booleanAttribute,
33
Component,
4-
HostBinding,
5-
Input,
6-
OnChanges,
4+
computed,
5+
effect,
6+
inject,
7+
input,
78
OnDestroy,
89
Renderer2,
9-
SimpleChanges,
1010
TemplateRef,
11-
ViewChild,
11+
viewChild,
1212
ViewContainerRef
1313
} from '@angular/core';
1414

1515
@Component({
1616
selector: 'c-tooltip',
1717
templateUrl: './tooltip.component.html',
1818
standalone: true,
19-
host: { class: 'tooltip fade bs-tooltip-auto' }
19+
host: {
20+
class: 'tooltip fade bs-tooltip-auto',
21+
'[class]': 'hostClasses()',
22+
'[attr.role]': 'role()',
23+
'[attr.id]': 'id()'
24+
}
2025
})
21-
export class TooltipComponent implements AfterViewInit, OnChanges, OnDestroy {
26+
export class TooltipComponent implements OnDestroy {
27+
readonly renderer = inject(Renderer2);
28+
2229
/**
2330
* Content of tooltip
2431
* @type {string | TemplateRef}
2532
*/
26-
@Input() content: string | TemplateRef<any> = '';
33+
readonly content = input<string | TemplateRef<any>>('');
34+
35+
readonly contentEffect = effect(() => {
36+
this.updateView(this.content());
37+
});
38+
2739
/**
2840
* Toggle the visibility of popover component.
2941
* @type boolean
3042
*/
31-
@Input() visible = false;
32-
@Input() @HostBinding('attr.id') id?: string;
33-
@Input() @HostBinding('attr.role') role = 'tooltip';
43+
readonly visible = input(false, { transform: booleanAttribute });
44+
readonly id = input<string>();
45+
readonly role = input('tooltip');
3446

35-
@ViewChild('tooltipTemplate', { read: ViewContainerRef }) viewContainerRef!: ViewContainerRef;
47+
readonly viewContainerRef = viewChild('tooltipTemplate', { read: ViewContainerRef });
3648
private textNode!: Text;
3749

38-
constructor(private renderer: Renderer2) {}
39-
40-
@HostBinding('class')
41-
get hostClasses(): { [klass: string]: any } {
50+
readonly hostClasses = computed<Record<string, boolean>>(() => {
4251
return {
4352
tooltip: true,
4453
fade: true,
45-
show: this.visible,
54+
show: this.visible(),
4655
'bs-tooltip-auto': true
4756
};
48-
}
49-
50-
ngAfterViewInit(): void {
51-
setTimeout(() => {
52-
this.updateView(this.content);
53-
});
54-
}
55-
56-
ngOnChanges(changes: SimpleChanges): void {
57-
if (changes['content']) {
58-
setTimeout(() => {
59-
this.updateView(this.content);
60-
});
61-
}
62-
}
57+
});
6358

6459
ngOnDestroy(): void {
6560
this.clear();
6661
}
6762

6863
private clear(): void {
69-
this.viewContainerRef?.clear();
64+
this.viewContainerRef()?.clear();
7065
if (!!this.textNode) {
7166
this.renderer.removeChild(this.textNode.parentNode, this.textNode);
7267
}
@@ -80,11 +75,11 @@ export class TooltipComponent implements AfterViewInit, OnChanges, OnDestroy {
8075
}
8176

8277
if (content instanceof TemplateRef) {
83-
this.viewContainerRef.createEmbeddedView(content);
78+
this.viewContainerRef()?.createEmbeddedView(content);
8479
} else {
8580
this.textNode = this.renderer.createText(content);
8681

87-
const element = this.viewContainerRef.element.nativeElement;
82+
const element = this.viewContainerRef()?.element.nativeElement;
8883
this.renderer.appendChild(element.parentNode, this.textNode);
8984
}
9085
}

0 commit comments

Comments
 (0)