Skip to content

Commit a922612

Browse files
committed
refactor(collapse): input signals, host bindings
1 parent e685b48 commit a922612

File tree

3 files changed

+86
-116
lines changed

3 files changed

+86
-116
lines changed
Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
11
import { animate, animation, style } from '@angular/animations';
22

3-
export const expandAnimation = animation([
4-
animate('{{ time }} {{ easing }}')
5-
]);
3+
export const expandAnimation = animation([animate('{{ time }} {{ easing }}')]);
64

75
export const collapseAnimation = animation([
86
style({ height: '*', minHeight: '*' }),
9-
animate('{{ time }} {{ easing }}',
10-
style({ height: 0, minHeight: 0 })
11-
)
7+
animate('{{ time }} {{ easing }}', style({ height: 0, minHeight: 0 }))
128
]);
139

14-
export const expandHorizontalAnimation = animation([
15-
animate('{{ time }} {{ easing }}')
16-
]);
10+
export const expandHorizontalAnimation = animation([animate('{{ time }} {{ easing }}')]);
1711

18-
export const collapseHorizontalAnimation = animation([
19-
// style({ opacity: '*' }),
20-
animate(
21-
'{{ time }} {{ easing }}'
22-
// style({ opacity: 0 })
23-
)
24-
]);
12+
export const collapseHorizontalAnimation = animation([animate('{{ time }} {{ easing }}')]);

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { CollapseDirective } from './collapse.directive';
2-
import { Component, DebugElement, ElementRef, Renderer2, Type } from '@angular/core';
3-
import { AnimationBuilder } from '@angular/animations';
2+
import { Component, DebugElement, ElementRef, Renderer2 } from '@angular/core';
43
import { ComponentFixture, TestBed } from '@angular/core/testing';
5-
import { By } from '@angular/platform-browser';
64
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
5+
import { By } from '@angular/platform-browser';
76

87
class MockElementRef extends ElementRef {}
98

109
@Component({
11-
template: '<div cCollapse horizontal></div>',
10+
template: '<div cCollapse horizontal>Test</div>',
1211
standalone: true,
1312
imports: [CollapseDirective]
1413
})
@@ -19,25 +18,23 @@ describe('CollapseDirective', () => {
1918
let fixture: ComponentFixture<TestComponent>;
2019
let elementRef: DebugElement;
2120
let renderer: Renderer2;
22-
let animationBuilder: AnimationBuilder;
2321

2422
beforeEach(() => {
2523
TestBed.configureTestingModule({
2624
imports: [TestComponent, CollapseDirective, NoopAnimationsModule],
27-
providers: [{ provide: ElementRef, useClass: MockElementRef }, { provide: AnimationBuilder }, Renderer2]
25+
providers: [{ provide: ElementRef, useClass: MockElementRef }, Renderer2]
2826
});
2927

3028
fixture = TestBed.createComponent(TestComponent);
3129
component = fixture.componentInstance;
3230
elementRef = fixture.debugElement.query(By.directive(CollapseDirective));
33-
renderer = fixture.componentRef.injector.get(Renderer2 as Type<Renderer2>);
3431

3532
fixture.detectChanges(); // initial binding
3633
});
3734

3835
it('should create an instance', () => {
3936
TestBed.runInInjectionContext(() => {
40-
const directive = new CollapseDirective(elementRef, renderer, animationBuilder);
37+
const directive = new CollapseDirective();
4138
expect(directive).toBeTruthy();
4239
});
4340
});
Lines changed: 77 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import {
2-
AfterViewInit,
32
booleanAttribute,
43
computed,
54
Directive,
6-
DoCheck,
5+
effect,
76
ElementRef,
8-
Input,
7+
inject,
98
input,
10-
OnChanges,
119
OnDestroy,
1210
output,
1311
Renderer2,
14-
SimpleChanges
12+
signal
1513
} from '@angular/core';
1614
import { AnimationBuilder, AnimationPlayer, useAnimation } from '@angular/animations';
1715

@@ -22,19 +20,31 @@ import {
2220
expandHorizontalAnimation
2321
} from './collapse.animations';
2422

25-
// todo
2623
@Directive({
2724
selector: '[cCollapse]',
2825
exportAs: 'cCollapse',
2926
standalone: true,
30-
host: { '[class]': 'hostClasses()' }
27+
host: { '[class]': 'hostClasses()', '[style]': '{display: "none"}' }
3128
})
32-
export class CollapseDirective implements OnDestroy, AfterViewInit, DoCheck, OnChanges {
29+
export class CollapseDirective implements OnDestroy {
30+
readonly #hostElement = inject(ElementRef);
31+
readonly #renderer = inject(Renderer2);
32+
readonly #animationBuilder = inject(AnimationBuilder);
33+
#player: AnimationPlayer | undefined = undefined;
34+
3335
/**
3436
* @ignore
35-
* todo: 'animate' input signal for navbar
3637
*/
37-
@Input({ transform: booleanAttribute }) animate: boolean = true;
38+
readonly animateInput = input(true, { transform: booleanAttribute, alias: 'animate' });
39+
40+
readonly animate = signal(true);
41+
42+
readonly animateInputEffect = effect(
43+
() => {
44+
this.animate.set(this.animateInput());
45+
},
46+
{ allowSignalWrites: true }
47+
);
3848

3949
/**
4050
* Set horizontal collapsing to transition the width instead of height.
@@ -47,18 +57,31 @@ export class CollapseDirective implements OnDestroy, AfterViewInit, DoCheck, OnC
4757
* Toggle the visibility of collapsible element.
4858
* @type boolean
4959
* @default false
50-
* todo: 'visible' input signal
5160
*/
52-
@Input({ transform: booleanAttribute })
53-
set visible(value) {
54-
this._visible = value;
55-
}
61+
readonly visibleInput = input(false, { transform: booleanAttribute, alias: 'visible' });
5662

57-
get visible(): boolean {
58-
return this._visible;
59-
}
63+
readonly visibleChange = output<boolean>();
64+
65+
readonly visibleInputEffect = effect(
66+
() => {
67+
this.visible.set(this.visibleInput());
68+
},
69+
{ allowSignalWrites: true }
70+
);
71+
72+
readonly visible = signal<boolean>(false);
6073

61-
private _visible: boolean = false;
74+
#init = false;
75+
76+
readonly visibleEffect = effect(
77+
() => {
78+
const visible = this.visible();
79+
80+
(this.#init || visible) && this.createPlayer(visible);
81+
this.#init = true;
82+
},
83+
{ allowSignalWrites: true }
84+
);
6285

6386
/**
6487
* Add `navbar` prop for grouping and hiding navbar contents by a parent breakpoint.
@@ -83,71 +106,38 @@ export class CollapseDirective implements OnDestroy, AfterViewInit, DoCheck, OnC
83106
*/
84107
readonly collapseChange = output<string>();
85108

86-
private player!: AnimationPlayer;
87-
private readonly host: HTMLElement;
88-
// private scrollHeight!: number;
89-
private scrollWidth!: number;
90-
private collapsing: boolean = false;
91-
92-
constructor(
93-
private readonly hostElement: ElementRef,
94-
private readonly renderer: Renderer2,
95-
private readonly animationBuilder: AnimationBuilder
96-
) {
97-
this.host = this.hostElement.nativeElement;
98-
this.renderer.setStyle(this.host, 'display', 'none');
99-
}
100-
101109
readonly hostClasses = computed(() => {
102110
return {
103111
'navbar-collapse': this.navbar(),
104112
'collapse-horizontal': this.horizontal()
105113
} as Record<string, boolean>;
106114
});
107115

108-
ngAfterViewInit(): void {
109-
if (this.visible) {
110-
this.toggle();
111-
}
112-
}
113-
114116
ngOnDestroy(): void {
115117
this.destroyPlayer();
116118
}
117119

118-
ngOnChanges(changes: SimpleChanges): void {
119-
if (changes['visible']) {
120-
if (!changes['visible'].firstChange || !changes['visible'].currentValue) {
121-
this.toggle(changes['visible'].currentValue);
122-
}
123-
}
124-
}
125-
126-
ngDoCheck(): void {
127-
if (this._visible !== this.visible) {
128-
this.toggle();
129-
}
130-
}
131-
132-
toggle(visible = this.visible): void {
133-
this.createPlayer(visible);
134-
this.player?.play();
120+
toggle(visible = !this.visible()): void {
121+
this.visible.set(visible);
135122
}
136123

137124
destroyPlayer(): void {
138-
this.player?.destroy();
125+
this.#player?.destroy();
126+
this.#player = undefined;
139127
}
140128

141-
createPlayer(visible: boolean = this.visible): void {
142-
if (this.player?.hasStarted()) {
129+
createPlayer(visible: boolean = this.visible()): void {
130+
if (this.#player?.hasStarted()) {
143131
this.destroyPlayer();
144132
}
145133

134+
const host: HTMLElement = this.#hostElement.nativeElement;
135+
146136
if (visible) {
147-
this.renderer.removeStyle(this.host, 'display');
137+
this.#renderer.removeStyle(host, 'display');
148138
}
149139

150-
const duration = this.animate ? this.duration() : '0ms';
140+
const duration = this.animate() ? this.duration() : '0ms';
151141

152142
const expand = this.horizontal() ? expandHorizontalAnimation : expandAnimation;
153143
const collapse = this.horizontal() ? collapseHorizontalAnimation : collapseAnimation;
@@ -156,53 +146,48 @@ export class CollapseDirective implements OnDestroy, AfterViewInit, DoCheck, OnC
156146
const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);
157147
const scrollSize = `scroll${capitalizedDimension}`;
158148

159-
const animationFactory = this.animationBuilder?.build(
149+
const animationFactory = this.#animationBuilder?.build(
160150
useAnimation(visible ? expand : collapse, { params: { time: duration, easing: this.transition() } })
161151
);
162152

163-
this.player = animationFactory.create(this.host);
153+
this.#player = animationFactory.create(host);
164154

165-
this.renderer.setStyle(this.host, dimension, visible ? 0 : `${this.host.getBoundingClientRect()[dimension]}px`);
155+
!visible && host.offsetHeight && host.style[dimension] && host.scrollHeight;
166156

167-
!visible && this.host.offsetHeight;
157+
this.#renderer.setStyle(host, dimension, visible ? 0 : `${host.getBoundingClientRect()[dimension]}px`);
168158

169-
this.player.onStart(() => {
159+
this.#player.onStart(() => {
170160
this.setMaxSize();
171-
this.renderer.removeClass(this.host, 'collapse');
172-
this.renderer.addClass(this.host, 'collapsing');
173-
this.renderer.removeClass(this.host, 'show');
174-
this.collapsing = true;
175-
if (visible) {
176-
this.renderer.setStyle(this.host, dimension, `${this.hostElement.nativeElement[scrollSize]}px`);
177-
} else {
178-
this.renderer.setStyle(this.host, dimension, '');
179-
}
180-
this.collapseChange.emit(visible ? 'opening' : 'collapsing');
161+
this.#renderer.removeClass(host, 'collapse');
162+
this.#renderer.addClass(host, 'collapsing');
163+
this.#renderer.removeClass(host, 'show');
164+
this.#renderer.setStyle(host, dimension, visible ? `${(host as any)[scrollSize]}px` : '');
165+
this.collapseChange?.emit(visible ? 'opening' : 'collapsing');
181166
});
182-
this.player.onDone(() => {
183-
this.visible = visible;
184-
this.collapsing = false;
185-
this.renderer.removeClass(this.host, 'collapsing');
186-
this.renderer.addClass(this.host, 'collapse');
167+
168+
this.#player.onDone(() => {
169+
this.#renderer.removeClass(host, 'collapsing');
170+
this.#renderer.addClass(host, 'collapse');
187171
if (visible) {
188-
this.renderer.addClass(this.host, 'show');
189-
this.renderer.setStyle(this.host, dimension, '');
172+
this.#renderer.addClass(host, 'show');
173+
this.#renderer.setStyle(host, dimension, '');
190174
} else {
191-
this.renderer.removeClass(this.host, 'show');
175+
this.#renderer.removeClass(host, 'show');
192176
}
193-
this.collapseChange.emit(visible ? 'open' : 'collapsed');
177+
this.collapseChange?.emit(visible ? 'open' : 'collapsed');
178+
this.destroyPlayer();
179+
this.visibleChange.emit(visible);
194180
});
181+
182+
this.#player?.play();
195183
}
196184

197185
setMaxSize() {
198-
// setTimeout(() => {
186+
const host = this.#hostElement.nativeElement;
199187
if (this.horizontal()) {
200-
this.scrollWidth = this.host.scrollWidth;
201-
this.scrollWidth > 0 && this.renderer.setStyle(this.host, 'maxWidth', `${this.scrollWidth}px`);
188+
host.scrollWidth > 0 && this.#renderer.setStyle(host, 'maxWidth', `${host.scrollWidth}px`);
202189
// } else {
203-
// this.scrollHeight = this.host.scrollHeight;
204-
// this.scrollHeight > 0 && this.renderer.setStyle(this.host, 'maxHeight', `${this.scrollHeight}px`);
190+
// host.scrollHeight > 0 && this.#renderer.setStyle(host, 'maxHeight', `${host.scrollHeight}px`);
205191
}
206-
// });
207192
}
208193
}

0 commit comments

Comments
 (0)