Skip to content

Commit 34007f4

Browse files
committed
refactor(alert): signal inputs, host bindings, cleanup, tests
1 parent fb32db2 commit 34007f4

File tree

3 files changed

+118
-90
lines changed

3 files changed

+118
-90
lines changed

projects/coreui-angular/src/lib/alert/alert.component.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
@if (visible || !hide) {
2-
@if (dismissible) {
3-
<ng-container *ngTemplateOutlet="templates?.alertButtonCloseTemplate || defaultAlertButtonCloseTemplate" />
1+
@if (visible || !hide()) {
2+
@if (dismissible()) {
3+
<ng-container *ngTemplateOutlet="templates()?.['alertButtonCloseTemplate'] || defaultAlertButtonCloseTemplate" />
44
}
55
<ng-content />
66
}

projects/coreui-angular/src/lib/alert/alert.component.spec.ts

+30-4
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,55 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
22
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
33

44
import { AlertComponent } from './alert.component';
5+
import { ComponentRef } from '@angular/core';
56

67
describe('AlertComponent', () => {
78
let component: AlertComponent;
9+
let componentRef: ComponentRef<AlertComponent>;
810
let fixture: ComponentFixture<AlertComponent>;
911

1012
beforeEach(waitForAsync(() => {
1113
TestBed.configureTestingModule({
12-
imports: [BrowserAnimationsModule, AlertComponent]
13-
})
14-
.compileComponents();
14+
imports: [BrowserAnimationsModule, AlertComponent, BrowserAnimationsModule]
15+
}).compileComponents();
1516
}));
1617

1718
beforeEach(() => {
1819
fixture = TestBed.createComponent(AlertComponent);
1920
component = fixture.componentInstance;
21+
componentRef = fixture.componentRef;
2022
fixture.detectChanges();
2123
});
2224

2325
it('should create', () => {
2426
expect(component).toBeTruthy();
2527
});
2628

27-
it('should have css classes', () => {
29+
it('should have css classes and styles', () => {
2830
expect(fixture.nativeElement).toHaveClass('alert');
31+
expect(fixture.nativeElement).toHaveClass('alert-primary');
32+
expect(fixture.nativeElement).toHaveClass('show');
33+
expect(fixture.nativeElement.style.opacity).toBe('1');
34+
componentRef.setInput('visible', false);
35+
componentRef.setInput('color', 'danger');
36+
fixture.detectChanges();
37+
expect(fixture.nativeElement).toHaveClass('alert-danger');
38+
expect(fixture.nativeElement.style.opacity).toBe('0');
39+
expect(fixture.nativeElement.style.height).toBe('0px');
40+
componentRef.setInput('dismissible', true);
41+
componentRef.setInput('fade', true);
42+
componentRef.setInput('variant', 'solid');
43+
componentRef.setInput('visible', true);
44+
fixture.detectChanges();
45+
expect(fixture.nativeElement).toHaveClass('alert-dismissible');
46+
expect(fixture.nativeElement).toHaveClass('fade');
47+
expect(fixture.nativeElement).not.toHaveClass('alert-danger');
48+
expect(fixture.nativeElement).toHaveClass('bg-danger');
49+
expect(fixture.nativeElement).toHaveClass('text-white');
50+
expect(fixture.nativeElement.style).toHaveSize(0);
51+
});
52+
53+
it('should have attributes', () => {
54+
expect(fixture.nativeElement.getAttribute('role')).toBe('alert');
2955
});
3056
});

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

+85-83
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {
2-
AfterContentInit,
32
booleanAttribute,
43
Component,
5-
ContentChildren,
6-
EventEmitter,
7-
HostBinding,
8-
HostListener,
9-
Input,
10-
Output,
11-
QueryList
4+
computed,
5+
contentChildren,
6+
effect,
7+
input,
8+
output,
9+
signal,
10+
TemplateRef
1211
} from '@angular/core';
1312
import { NgTemplateOutlet } from '@angular/common';
1413
import { animate, AnimationEvent, state, style, transition, trigger } from '@angular/animations';
@@ -17,139 +16,142 @@ import { Colors } from '../coreui.types';
1716
import { TemplateIdDirective } from '../shared';
1817
import { ButtonCloseDirective } from '../button';
1918

20-
type AnimateType = ('hide' | 'show');
19+
type AnimateType = 'hide' | 'show';
2120

2221
@Component({
23-
selector: 'c-alert',
24-
templateUrl: './alert.component.html',
25-
styleUrls: ['./alert.component.scss'],
26-
exportAs: 'cAlert',
27-
imports: [NgTemplateOutlet, ButtonCloseDirective],
28-
animations: [
29-
trigger('fadeInOut', [
30-
state('show', style({ opacity: 1, height: '*', padding: '*', border: '*', margin: '*' })),
31-
state('hide', style({ opacity: 0, height: 0, padding: 0, border: 0, margin: 0 })),
32-
state('void', style({ opacity: 0, height: 0, padding: 0, border: 0, margin: 0 })),
33-
transition('show => hide', [
34-
animate('.3s ease-out')
35-
]),
36-
transition('hide => show', [
37-
animate('.3s ease-in')
38-
]),
39-
transition('show => void', [
40-
animate('.3s ease-out')
41-
]),
42-
transition('void => show', [
43-
animate('.3s ease-in')
44-
])
45-
])
46-
]
22+
selector: 'c-alert',
23+
templateUrl: './alert.component.html',
24+
styleUrls: ['./alert.component.scss'],
25+
exportAs: 'cAlert',
26+
imports: [NgTemplateOutlet, ButtonCloseDirective],
27+
animations: [
28+
trigger('fadeInOut', [
29+
state('show', style({ opacity: 1, height: '*', padding: '*', border: '*', margin: '*' })),
30+
state('hide', style({ opacity: 0, height: 0, padding: 0, border: 0, margin: 0 })),
31+
state('void', style({ opacity: 0, height: 0, padding: 0, border: 0, margin: 0 })),
32+
transition('show => hide', [animate('.3s ease-out')]),
33+
transition('hide => show', [animate('.3s ease-in')]),
34+
transition('show => void', [animate('.3s ease-out')]),
35+
transition('void => show', [animate('.3s ease-in')])
36+
])
37+
],
38+
host: {
39+
'[@.disabled]': '!fade()',
40+
'[@fadeInOut]': 'animateType',
41+
'[attr.role]': 'role()',
42+
'[class]': 'hostClasses()',
43+
'(@fadeInOut.start)': 'onAnimationStart($event)',
44+
'(@fadeInOut.done)': 'onAnimationDone($event)'
45+
}
4746
})
48-
export class AlertComponent implements AfterContentInit {
49-
50-
hide!: boolean;
47+
export class AlertComponent {
5148
/**
5249
* Sets the color context of the component to one of CoreUI’s themed colors.
53-
*
54-
* @type Colors
50+
* @return Colors
5551
* @default 'primary'
5652
*/
57-
@Input() color: Colors = 'primary';
53+
readonly color = input<Colors>('primary');
54+
5855
/**
5956
* Default role for alert. [docs]
60-
* @type string
57+
* @return string
6158
* @default 'alert'
6259
*/
63-
@HostBinding('attr.role')
64-
@Input() role = 'alert';
60+
readonly role = input('alert');
61+
6562
/**
6663
* Set the alert variant to a solid.
67-
* @type string
68-
*/
69-
@Input() variant?: 'solid' | string;
70-
/**
71-
* Event triggered on the alert dismiss.
64+
* @return string
7265
*/
73-
@Output() visibleChange: EventEmitter<boolean> = new EventEmitter();
74-
templates: any = {};
75-
@ContentChildren(TemplateIdDirective, { descendants: true }) contentTemplates!: QueryList<TemplateIdDirective>;
66+
readonly variant = input<'solid'>();
7667

7768
/**
7869
* Optionally adds a close button to alert and allow it to self dismiss.
79-
* @type boolean
70+
* @return boolean
8071
* @default false
8172
*/
82-
@Input({ transform: booleanAttribute }) dismissible: boolean = false;
73+
readonly dismissible = input(false, { transform: booleanAttribute });
8374

8475
/**
8576
* Adds animation for dismissible alert.
86-
* @type boolean
77+
* @return boolean
8778
*/
88-
@Input({ transform: booleanAttribute }) fade: boolean = false;
79+
readonly fade = input(false, { transform: booleanAttribute });
8980

9081
/**
9182
* Toggle the visibility of alert component.
92-
* @type boolean
83+
* @return boolean
9384
*/
94-
@Input({ transform: booleanAttribute })
85+
readonly visibleInput = input(true, { transform: booleanAttribute, alias: 'visible' });
86+
87+
readonly #visibleInputEffect = effect(() => {
88+
this.visible = this.visibleInput();
89+
});
90+
9591
set visible(value: boolean) {
9692
if (this.#visible !== value) {
9793
this.#visible = value;
9894
this.visibleChange.emit(value);
9995
}
100-
};
96+
}
10197

10298
get visible() {
10399
return this.#visible;
104100
}
105101

106102
#visible: boolean = true;
107103

108-
@HostBinding('@.disabled')
109-
get animationDisabled(): boolean {
110-
return !this.fade;
111-
}
104+
readonly hide = signal<boolean>(false);
105+
106+
/**
107+
* Event triggered on the alert dismiss.
108+
*/
109+
readonly visibleChange = output<boolean>();
110+
111+
readonly contentTemplates = contentChildren(TemplateIdDirective, { descendants: true });
112+
113+
readonly templates = computed(() => {
114+
return this.contentTemplates().reduce(
115+
(acc, child) => {
116+
acc[child.id] = child.templateRef;
117+
return acc;
118+
},
119+
{} as Record<string, TemplateRef<any>>
120+
);
121+
});
112122

113-
@HostBinding('@fadeInOut')
114123
get animateType(): AnimateType {
115124
return this.visible ? 'show' : 'hide';
116125
}
117126

118-
@HostBinding('class')
119-
get hostClasses(): any {
127+
readonly hostClasses = computed(() => {
128+
const color = this.color();
129+
const variant = this.variant();
120130
return {
121131
alert: true,
122-
'alert-dismissible': this.dismissible,
123-
fade: this.fade,
124-
show: !this.hide,
125-
[`alert-${this.color}`]: !!this.color && this.variant !== 'solid',
126-
[`bg-${this.color}`]: !!this.color && this.variant === 'solid',
127-
'text-white': !!this.color && this.variant === 'solid'
128-
};
129-
}
132+
'alert-dismissible': this.dismissible(),
133+
fade: this.fade(),
134+
show: !this.hide(),
135+
[`alert-${color}`]: !!color && variant !== 'solid',
136+
[`bg-${color}`]: !!color && variant === 'solid',
137+
'text-white': !!color && variant === 'solid'
138+
} as Record<string, boolean>;
139+
});
130140

131-
@HostListener('@fadeInOut.start', ['$event'])
132141
onAnimationStart($event: AnimationEvent): void {
133142
this.onAnimationEvent($event);
134143
}
135144

136-
@HostListener('@fadeInOut.done', ['$event'])
137145
onAnimationDone($event: AnimationEvent): void {
138146
this.onAnimationEvent($event);
139147
}
140148

141-
ngAfterContentInit(): void {
142-
this.contentTemplates.forEach((child: TemplateIdDirective) => {
143-
this.templates[child.id] = child.templateRef;
144-
});
145-
}
146-
147149
onAnimationEvent(event: AnimationEvent): void {
148-
this.hide = event.phaseName === 'start' && event.toState === 'show';
150+
this.hide.set(event.phaseName === 'start' && event.toState === 'show');
149151
if (event.phaseName === 'done') {
150-
this.hide = (event.toState === 'hide' || event.toState === 'void');
152+
this.hide.set(event.toState === 'hide' || event.toState === 'void');
151153
if (event.toState === 'show') {
152-
this.hide = false;
154+
this.hide.set(false);
153155
}
154156
}
155157
}

0 commit comments

Comments
 (0)