Skip to content

Commit b9943fb

Browse files
committed
refactor(form-check): signal inputs, host bindings, cleanup
1 parent 6b74c32 commit b9943fb

File tree

5 files changed

+153
-71
lines changed

5 files changed

+153
-71
lines changed

projects/coreui-angular/src/lib/form/form-check/form-check-input.directive.spec.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, DebugElement, ElementRef, Renderer2, Type } from '@angular/core';
1+
import { Component, DebugElement, ElementRef, Renderer2 } from '@angular/core';
22
import { ComponentFixture, TestBed } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { FormCheckInputDirective } from './form-check-input.directive';
@@ -15,7 +15,7 @@ describe('FormCheckInputDirective', () => {
1515
let component: TestComponent;
1616
let fixture: ComponentFixture<TestComponent>;
1717
let inputEl: DebugElement;
18-
let renderer: Renderer2;
18+
// let renderer: Renderer2;
1919

2020
beforeEach(() => {
2121
TestBed.configureTestingModule({
@@ -24,8 +24,8 @@ describe('FormCheckInputDirective', () => {
2424
});
2525
fixture = TestBed.createComponent(TestComponent);
2626
component = fixture.componentInstance;
27-
inputEl = fixture.debugElement.query(By.css('input'));
28-
renderer = fixture.componentRef.injector.get(Renderer2 as Type<Renderer2>);
27+
inputEl = fixture.debugElement.query(By.directive(FormCheckInputDirective));
28+
// renderer = fixture.componentRef.injector.get(Renderer2 as Type<Renderer2>);
2929
});
3030

3131
it('should create an instance', () => {
@@ -34,4 +34,8 @@ describe('FormCheckInputDirective', () => {
3434
expect(directive).toBeTruthy();
3535
});
3636
});
37+
38+
it('should have css classes', () => {
39+
expect(inputEl.nativeElement).toHaveClass('form-check-input');
40+
});
3741
});

projects/coreui-angular/src/lib/form/form-check/form-check-input.directive.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,86 @@
1-
import { booleanAttribute, Directive, ElementRef, HostBinding, inject, Input, Renderer2 } from '@angular/core';
1+
import {
2+
booleanAttribute,
3+
computed,
4+
Directive,
5+
effect,
6+
ElementRef,
7+
inject,
8+
input,
9+
Renderer2,
10+
signal,
11+
untracked
12+
} from '@angular/core';
213

314
@Directive({
415
selector: 'input[cFormCheckInput]',
5-
host: { class: 'form-check-input' }
16+
host: {
17+
class: 'form-check-input',
18+
'[class]': 'hostClasses()',
19+
'[attr.type]': 'type()'
20+
}
621
})
722
export class FormCheckInputDirective {
823
readonly #renderer = inject(Renderer2);
924
readonly #hostElement = inject(ElementRef);
1025

1126
/**
1227
* Specifies the type of component.
13-
* @type {'checkbox' | 'radio'}
1428
* @default 'checkbox'
1529
*/
16-
@HostBinding('attr.type')
17-
@Input()
18-
type: 'checkbox' | 'radio' = 'checkbox';
30+
readonly type = input<'checkbox' | 'radio'>('checkbox');
1931

2032
/**
2133
* Set component indeterminate state.
22-
* @type boolean
2334
* @default false
2435
*/
25-
@Input({ transform: booleanAttribute })
26-
set indeterminate(value: boolean) {
27-
const indeterminate = value;
28-
if (this._indeterminate !== indeterminate) {
29-
this._indeterminate = indeterminate;
36+
readonly indeterminateInput = input(false, { transform: booleanAttribute, alias: 'indeterminate' });
37+
38+
readonly #indeterminateEffect = effect(() => {
39+
const indeterminate = this.indeterminateInput();
40+
if (untracked(this.#indeterminate) !== indeterminate) {
3041
const htmlInputElement = this.#hostElement.nativeElement as HTMLInputElement;
3142
if (indeterminate) {
3243
this.#renderer.setProperty(htmlInputElement, 'checked', false);
3344
}
3445
this.#renderer.setProperty(htmlInputElement, 'indeterminate', indeterminate);
46+
this.#indeterminate.set(indeterminate);
3547
}
36-
}
48+
});
3749

3850
get indeterminate() {
39-
return this._indeterminate;
51+
return this.#indeterminate();
4052
}
4153

42-
private _indeterminate = false;
54+
readonly #indeterminate = signal(false);
4355

4456
/**
4557
* Set component validation state to valid.
46-
* @type boolean
4758
* @default undefined
4859
*/
49-
@Input() valid?: boolean;
60+
readonly valid = input<boolean>();
5061

51-
@HostBinding('class')
52-
get hostClasses(): any {
62+
readonly hostClasses = computed(() => {
63+
const valid = this.valid();
5364
return {
5465
'form-check-input': true,
55-
'is-valid': this.valid === true,
56-
'is-invalid': this.valid === false
57-
};
58-
}
66+
'is-valid': valid === true,
67+
'is-invalid': valid === false
68+
} as Record<string, boolean>;
69+
});
5970

60-
@Input({ transform: booleanAttribute })
61-
set checked(value: boolean) {
62-
const checked = value;
71+
/**
72+
* Set component checked state.
73+
* @default false
74+
*/
75+
readonly checkedInput = input(false, { transform: booleanAttribute, alias: 'checked' });
76+
77+
readonly #checkedEffect = effect(() => {
78+
const checked = this.checkedInput();
6379
const htmlInputElement = this.#hostElement?.nativeElement as HTMLInputElement;
6480
if (htmlInputElement) {
6581
this.#renderer.setProperty(htmlInputElement, 'checked', checked);
6682
}
67-
}
83+
});
6884

6985
get checked(): boolean {
7086
return this.#hostElement?.nativeElement?.checked;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
11
import { FormCheckLabelDirective } from './form-check-label.directive';
2+
import { Component, DebugElement } from '@angular/core';
3+
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
import { By } from '@angular/platform-browser';
5+
6+
@Component({
7+
template: '<label cFormCheckLabel>Label</label>',
8+
imports: [FormCheckLabelDirective]
9+
})
10+
class TestComponent {}
211

312
describe('FormCheckLabelDirective', () => {
13+
let component: TestComponent;
14+
let fixture: ComponentFixture<TestComponent>;
15+
let debugElement: DebugElement;
16+
beforeEach(() => {
17+
TestBed.configureTestingModule({
18+
imports: [TestComponent]
19+
});
20+
fixture = TestBed.createComponent(TestComponent);
21+
component = fixture.componentInstance;
22+
debugElement = fixture.debugElement.query(By.directive(FormCheckLabelDirective));
23+
});
24+
425
it('should create an instance', () => {
526
const directive = new FormCheckLabelDirective();
627
expect(directive).toBeTruthy();
728
});
29+
30+
it('should have css classes', () => {
31+
expect(debugElement.nativeElement).toHaveClass('form-check-label');
32+
});
833
});
Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,78 @@
1+
import { Component, ComponentRef, DebugElement } from '@angular/core';
12
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2-
33
import { FormCheckComponent } from './form-check.component';
4-
import { Renderer2 } from '@angular/core';
4+
import { FormCheckInputDirective } from './form-check-input.directive';
5+
import { FormCheckLabelDirective } from './form-check-label.directive';
6+
import { By } from '@angular/platform-browser';
7+
8+
@Component({
9+
template: `
10+
<c-form-check [inline]="inline" [reverse]="reverse" [switch]="switch" [sizing]="'xl'">
11+
<input cFormCheckInput id="check1" type="checkbox" />
12+
<label cFormCheckLabel for="check1">Label</label>
13+
</c-form-check>
14+
`,
15+
imports: [FormCheckInputDirective, FormCheckComponent, FormCheckLabelDirective]
16+
})
17+
class TestComponent {
18+
inline = true;
19+
reverse = true;
20+
switch = false;
21+
}
522

623
describe('FormCheckComponent', () => {
724
let component: FormCheckComponent;
825
let fixture: ComponentFixture<FormCheckComponent>;
9-
let renderer: Renderer2;
26+
let componentRef: ComponentRef<FormCheckComponent>;
1027

1128
beforeEach(waitForAsync(() => {
1229
TestBed.configureTestingModule({
13-
imports: [FormCheckComponent],
14-
providers: [Renderer2]
15-
})
16-
.compileComponents();
17-
}));
30+
imports: [FormCheckComponent]
31+
}).compileComponents();
1832

19-
beforeEach(() => {
2033
fixture = TestBed.createComponent(FormCheckComponent);
21-
renderer = fixture.debugElement.injector.get(Renderer2);
2234
component = fixture.componentInstance;
23-
component.switch = true;
35+
componentRef = fixture.componentRef;
36+
componentRef.setInput('switch', true);
2437
fixture.detectChanges();
25-
});
38+
}));
2639

2740
it('should create', () => {
2841
expect(component).toBeTruthy();
2942
});
3043

3144
it('should have css classes', () => {
3245
expect(fixture.nativeElement).toHaveClass('form-switch');
46+
expect(fixture.nativeElement).not.toHaveClass('form-check');
3347
});
48+
});
49+
50+
describe('FormCheckComponent Test', () => {
51+
let testFixture: ComponentFixture<TestComponent>;
52+
let debugElement: DebugElement;
3453

54+
beforeEach(waitForAsync(() => {
55+
TestBed.configureTestingModule({
56+
imports: [TestComponent]
57+
}).compileComponents();
58+
testFixture = TestBed.createComponent(TestComponent);
59+
debugElement = testFixture.debugElement.query(By.directive(FormCheckComponent));
60+
testFixture.detectChanges(); // initial binding
61+
}));
62+
63+
it('should have css classes', () => {
64+
expect(debugElement.nativeElement).not.toHaveClass('form-switch');
65+
expect(debugElement.nativeElement).not.toHaveClass('form-switch-xl');
66+
expect(debugElement.nativeElement).toHaveClass('form-check-inline');
67+
expect(debugElement.nativeElement).toHaveClass('form-check-reverse');
68+
testFixture.componentInstance.switch = true;
69+
testFixture.componentInstance.inline = false;
70+
testFixture.componentInstance.reverse = false;
71+
testFixture.detectChanges();
72+
expect(debugElement.nativeElement).toHaveClass('form-switch');
73+
expect(debugElement.nativeElement).toHaveClass('form-switch-xl');
74+
expect(debugElement.nativeElement).not.toHaveClass('form-check-inline');
75+
expect(debugElement.nativeElement).not.toHaveClass('form-check-reverse');
76+
expect(debugElement.nativeElement).toHaveClass('form-check');
77+
});
3578
});
Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,54 @@
1-
import { AfterContentInit, booleanAttribute, Component, ContentChild, HostBinding, Input } from '@angular/core';
1+
import { booleanAttribute, Component, computed, contentChild, input } from '@angular/core';
22

33
import { FormCheckLabelDirective } from './form-check-label.directive';
44

55
@Component({
66
selector: 'c-form-check',
77
template: '<ng-content />',
8-
exportAs: 'cFormCheck'
8+
exportAs: 'cFormCheck',
9+
host: { '[class]': 'hostClasses()' }
910
})
10-
export class FormCheckComponent implements AfterContentInit {
11+
export class FormCheckComponent {
1112
/**
1213
* Group checkboxes or radios on the same horizontal row.
13-
* @type boolean
1414
* @default false
1515
*/
16-
@Input({ transform: booleanAttribute }) inline: string | boolean = false;
16+
readonly inline = input(false, { transform: booleanAttribute });
1717

1818
/**
1919
* Put checkboxes or radios on the opposite side.
20-
* @type boolean
2120
* @default false
2221
* @since 4.4.7
2322
*/
24-
@Input({ transform: booleanAttribute }) reverse: string | boolean = false;
23+
readonly reverse = input(false, { transform: booleanAttribute });
2524

2625
/**
2726
* Size the component large or extra large. Works only with `[switch]="true"` [docs]
28-
* @type {'lg' | 'xl' | ''}
27+
* @default undefined
2928
*/
30-
@Input() sizing?: 'lg' | 'xl' | '' = '';
29+
readonly sizing = input<'' | 'lg' | 'xl' | string>();
3130

3231
/**
3332
* Render a toggle switch on for checkbox.
3433
* @type boolean
3534
* @default false
3635
*/
37-
@Input({ transform: booleanAttribute }) switch: string | boolean = false;
36+
readonly switch = input(false, { transform: booleanAttribute });
3837

39-
@HostBinding('class')
40-
get hostClasses(): any {
41-
return {
42-
'form-check': this.formCheckClass,
43-
'form-switch': this.switch,
44-
[`form-switch-${this.sizing}`]: this.switch && !!this.sizing,
45-
'form-check-inline': this.inline,
46-
'form-check-reverse': this.reverse
47-
};
48-
}
38+
readonly formCheckLabel = contentChild(FormCheckLabelDirective);
4939

50-
@ContentChild(FormCheckLabelDirective) formCheckLabel!: FormCheckLabelDirective;
40+
readonly formCheckClass = computed(() => !!this.formCheckLabel());
5141

52-
#formCheckClass = true;
53-
get formCheckClass() {
54-
return this.#formCheckClass;
55-
}
42+
readonly hostClasses = computed(() => {
43+
const sizing = this.sizing();
44+
const isSwitch = this.switch();
5645

57-
ngAfterContentInit(): void {
58-
this.#formCheckClass = !!this.formCheckLabel;
59-
}
46+
return {
47+
'form-check': !!this.formCheckLabel(),
48+
'form-switch': isSwitch,
49+
[`form-switch-${sizing}`]: isSwitch && !!sizing,
50+
'form-check-inline': this.inline(),
51+
'form-check-reverse': this.reverse()
52+
};
53+
});
6054
}

0 commit comments

Comments
 (0)