Skip to content

Commit 40b4c9a

Browse files
committed
refactor(grid): signal inputs, host bindings, cleanup, tests
1 parent fe69aa8 commit 40b4c9a

9 files changed

+247
-138
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,60 @@
1-
import { ColDirective } from './col.directive';
1+
import { Component, DebugElement } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { By } from '@angular/platform-browser';
4+
import { ColDirective, ColOffsetType, ColOrderType } from './col.directive';
5+
6+
@Component({
7+
imports: [ColDirective],
8+
template: `
9+
<div id="col0" cCol lg="auto" [xl]="true"></div>
10+
<div id="col1" cCol [order]="1" [offset]="1" xs="6" sm="5" md="4" lg="3" xl="2" xxl="1"></div>
11+
<div id="col2" [cCol]="col" [order]="order" [offset]="offset"></div>
12+
`
13+
})
14+
export class TestComponent {
15+
col!: number;
16+
offset: ColOffsetType = { md: 2, xs: 1 };
17+
order: ColOrderType = { xl: 'first', xxl: 'last', md: 1, xs: 1 };
18+
}
219

320
describe('ColDirective', () => {
21+
let fixture: ComponentFixture<TestComponent>;
22+
let debugElement: DebugElement;
23+
24+
beforeEach(() => {
25+
TestBed.configureTestingModule({
26+
imports: [TestComponent]
27+
});
28+
fixture = TestBed.createComponent(TestComponent);
29+
fixture.detectChanges();
30+
});
31+
432
it('should create an instance', () => {
5-
const directive = new ColDirective();
6-
expect(directive).toBeTruthy();
33+
TestBed.runInInjectionContext(() => {
34+
const directive = new ColDirective();
35+
expect(directive).toBeTruthy();
36+
});
37+
});
38+
39+
it('should have css class', () => {
40+
debugElement = fixture.debugElement.query(By.css('#col0'));
41+
expect(debugElement.nativeElement).toHaveClass('col');
42+
expect(debugElement.nativeElement).toHaveClass('col-lg-auto');
43+
expect(debugElement.nativeElement).toHaveClass('col-xl');
44+
debugElement = fixture.debugElement.query(By.css('#col1'));
45+
expect(debugElement.nativeElement).toHaveClass('col-6');
46+
expect(debugElement.nativeElement).toHaveClass('order-1');
47+
expect(debugElement.nativeElement).toHaveClass('offset-1');
48+
expect(debugElement.nativeElement).toHaveClass('col-sm-5');
49+
expect(debugElement.nativeElement).toHaveClass('col-md-4');
50+
expect(debugElement.nativeElement).toHaveClass('col-lg-3');
51+
expect(debugElement.nativeElement).toHaveClass('col-xl-2');
52+
expect(debugElement.nativeElement).toHaveClass('col-xxl-1');
53+
debugElement = fixture.debugElement.query(By.css('#col2'));
54+
expect(debugElement.nativeElement).toHaveClass('col');
55+
expect(debugElement.nativeElement).toHaveClass('offset-md-2');
56+
expect(debugElement.nativeElement).toHaveClass('order-md-1');
57+
expect(debugElement.nativeElement).toHaveClass('order-xl-first');
58+
expect(debugElement.nativeElement).toHaveClass('order-xxl-last');
759
});
860
});
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import { Directive, HostBinding, Input } from '@angular/core';
2-
import { BooleanInput, coerceBooleanProperty, coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';
3-
4-
import { ColOrder, ICol } from './col.type';
1+
import { booleanAttribute, computed, Directive, input, numberAttribute } from '@angular/core';
2+
import { BooleanInput, NumberInput } from '@angular/cdk/coercion';
53
import { BreakpointInfix } from '../coreui.types';
4+
import { ColOrder } from './col.type';
5+
6+
export type ColOffsetType = number | { xs?: number; sm?: number; md?: number; lg?: number; xl?: number; xxl?: number };
7+
export type ColOrderType =
8+
| ColOrder
9+
| { xs?: ColOrder; sm?: ColOrder; md?: ColOrder; lg?: ColOrder; xl?: ColOrder; xxl?: ColOrder };
610

711
@Directive({
8-
selector: '[cCol]'
12+
selector: '[cCol]',
13+
host: {
14+
'[class]': 'hostClasses()'
15+
}
916
})
10-
export class ColDirective implements ICol {
17+
export class ColDirective {
1118
static ngAcceptInputType_cCol: BooleanInput | NumberInput;
1219
static ngAcceptInputType_xs: BooleanInput | NumberInput;
1320
static ngAcceptInputType_sm: BooleanInput | NumberInput;
@@ -18,142 +25,111 @@ export class ColDirective implements ICol {
1825

1926
/**
2027
* The number of columns/offset/order on extra small devices (<576px).
21-
* @type { 'auto' | number | boolean }
28+
* @return { 'auto' | number | boolean }
2229
*/
23-
@Input()
24-
set cCol(value: BooleanInput | NumberInput) {
25-
this.xs = this.xs || this.coerceInput(value);
26-
}
27-
@Input()
28-
set xs(value) {
29-
this._xs = this.coerceInput(value);
30-
}
31-
get xs(): BooleanInput | NumberInput {
32-
return this._xs;
33-
}
34-
private _xs: BooleanInput | NumberInput = false;
30+
readonly cCol = input(false, { transform: this.coerceInput });
31+
readonly xs = input(false, { transform: this.coerceInput });
3532

3633
/**
3734
* The number of columns/offset/order on small devices (<768px).
38-
* @type { 'auto' | number | boolean }
35+
* @return { 'auto' | number | boolean }
3936
*/
40-
@Input()
41-
set sm(value) {
42-
this._sm = this.coerceInput(value);
43-
}
44-
get sm(): BooleanInput | NumberInput {
45-
return this._sm;
46-
}
47-
private _sm: BooleanInput | NumberInput = false;
37+
readonly sm = input(false, { transform: this.coerceInput });
4838

4939
/**
5040
* The number of columns/offset/order on medium devices (<992px).
51-
* @type { 'auto' | number | boolean }
41+
* @return { 'auto' | number | boolean }
5242
*/
53-
@Input()
54-
set md(value) {
55-
this._md = this.coerceInput(value);
56-
}
57-
get md(): BooleanInput | NumberInput {
58-
return this._md;
59-
}
60-
private _md: BooleanInput | NumberInput = false;
43+
readonly md = input(false, { transform: this.coerceInput });
6144

6245
/**
6346
* The number of columns/offset/order on large devices (<1200px).
64-
* @type { 'auto' | number | boolean }
47+
* @return { 'auto' | number | boolean }
6548
*/
66-
@Input()
67-
set lg(value) {
68-
this._lg = this.coerceInput(value);
69-
}
70-
get lg(): BooleanInput | NumberInput {
71-
return this._lg;
72-
}
73-
private _lg: BooleanInput | NumberInput = false;
49+
readonly lg = input(false, { transform: this.coerceInput });
7450

7551
/**
7652
* The number of columns/offset/order on X-Large devices (<1400px).
77-
* @type { 'auto' | number | boolean }
53+
* @return { 'auto' | number | boolean }
7854
*/
79-
@Input()
80-
set xl(value) {
81-
this._xl = this.coerceInput(value);
82-
}
83-
get xl(): BooleanInput | NumberInput {
84-
return this._xl;
85-
}
86-
private _xl: BooleanInput | NumberInput = false;
55+
readonly xl = input(false, { transform: this.coerceInput });
8756

8857
/**
8958
* The number of columns/offset/order on XX-Large devices (≥1400px).
90-
* @type { 'auto' | number | boolean }
59+
* @return { 'auto' | number | boolean }
9160
*/
92-
@Input()
93-
set xxl(value) {
94-
this._xxl = this.coerceInput(value);
95-
}
96-
get xxl(): BooleanInput | NumberInput {
97-
return this._xxl;
98-
}
99-
private _xxl: BooleanInput | NumberInput = false;
61+
readonly xxl = input(false, { transform: this.coerceInput });
62+
63+
readonly breakpoints = computed(() => {
64+
return {
65+
xs: this.xs() || this.cCol(),
66+
sm: this.sm(),
67+
md: this.md(),
68+
lg: this.lg(),
69+
xl: this.xl(),
70+
xxl: this.xxl()
71+
} as Record<string, any>;
72+
});
10073

101-
@Input() offset?: number | { xs?: number; sm?: number; md?: number; lg?: number; xl?: number; xxl?: number };
102-
@Input() order?:
103-
| ColOrder
104-
| { xs?: ColOrder; sm?: ColOrder; md?: ColOrder; lg?: ColOrder; xl?: ColOrder; xxl?: ColOrder };
74+
readonly offset = input<ColOffsetType>();
75+
readonly order = input<ColOrderType>();
10576

106-
@HostBinding('class')
107-
get hostClasses(): any {
108-
const classes: any = {
77+
readonly hostClasses = computed(() => {
78+
const classes: Record<string, boolean> = {
10979
col: true
11080
};
11181

82+
const breakpoints = this.breakpoints();
83+
const offsetInput = this.offset();
84+
const orderInput = this.order();
85+
11286
Object.keys(BreakpointInfix).forEach((breakpoint) => {
113-
// @ts-ignore
114-
const value: number | string | boolean = this[breakpoint];
87+
const value = breakpoints[breakpoint];
11588
const infix = breakpoint === 'xs' ? '' : `-${breakpoint}`;
11689
classes[`col${infix}`] = value === true;
11790
classes[`col${infix}-${value}`] = typeof value === 'number' || typeof value === 'string';
11891
});
11992

120-
if (typeof this.offset === 'object') {
121-
const offset = { ...this.offset };
93+
if (typeof offsetInput === 'object') {
94+
const offset = { ...offsetInput };
12295
Object.entries(offset).forEach((entry) => {
12396
const [breakpoint, value] = [...entry];
12497
const infix = breakpoint === 'xs' ? '' : `-${breakpoint}`;
12598
classes[`offset${infix}-${value}`] = value >= 0 && value <= 11;
12699
});
127100
} else {
128-
classes[`offset-${this.offset}`] = typeof this.offset === 'number' && this.offset > 0 && this.offset <= 11;
101+
const offset = numberAttribute(offsetInput);
102+
classes[`offset-${offset}`] = typeof offset === 'number' && offset > 0 && offset <= 11;
129103
}
130104

131-
if (typeof this.order === 'object') {
132-
const order = { ...this.order };
105+
if (typeof orderInput === 'object') {
106+
const order = { ...orderInput };
133107
Object.entries(order).forEach((entry) => {
134108
const [breakpoint, value] = [...entry];
135109
const infix = breakpoint === 'xs' ? '' : `-${breakpoint}`;
136-
classes[`order${infix}-${value}`] = value;
110+
classes[`order${infix}-${value}`] = !!value;
137111
});
138112
} else {
139-
classes[`order-${this.order}`] = !!this.order;
113+
const order = orderInput;
114+
classes[`order-${order}`] = !!order;
140115
}
141116

142117
// if there is no 'col' class, add one
143-
classes.col = !Object.entries(classes).filter((i) => i[0].startsWith('col-') && i[1]).length || this.xs === true;
144-
return classes;
145-
}
118+
classes['col'] =
119+
!Object.entries(classes).filter((i) => i[0].startsWith('col-') && i[1]).length || breakpoints['xs'] === true;
120+
return classes as Record<string, boolean>;
121+
});
146122

147123
coerceInput(value: BooleanInput | NumberInput) {
148124
if (value === 'auto') {
149125
return value;
150126
}
151127
if (value === '' || value === undefined || value === null) {
152-
return coerceBooleanProperty(value);
128+
return booleanAttribute(value);
153129
}
154130
if (typeof value === 'boolean') {
155131
return value;
156132
}
157-
return coerceNumberProperty(value);
133+
return numberAttribute(value);
158134
}
159135
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
22

33
import { ContainerComponent } from './container.component';
4+
import { ComponentRef } from '@angular/core';
45

56
describe('ContainerComponent', () => {
67
let component: ContainerComponent;
8+
let componentRef: ComponentRef<ContainerComponent>;
79
let fixture: ComponentFixture<ContainerComponent>;
810

911
beforeEach(waitForAsync(() => {
1012
TestBed.configureTestingModule({
1113
imports: [ContainerComponent]
12-
})
13-
.compileComponents();
14+
}).compileComponents();
1415
}));
1516

1617
beforeEach(() => {
1718
fixture = TestBed.createComponent(ContainerComponent);
1819
component = fixture.componentInstance;
20+
componentRef = fixture.componentRef;
1921
fixture.detectChanges();
2022
});
2123

@@ -25,5 +27,11 @@ describe('ContainerComponent', () => {
2527

2628
it('should have css classes', () => {
2729
expect(fixture.nativeElement).toHaveClass('container');
30+
expect(fixture.nativeElement).not.toHaveClass('container-fluid');
31+
expect(fixture.nativeElement).not.toHaveClass('container-xl');
32+
componentRef.setInput('fluid', true);
33+
componentRef.setInput('breakpoint', 'xl');
34+
fixture.detectChanges();
35+
expect(fixture.nativeElement).toHaveClass('container-xl');
2836
});
2937
});

projects/coreui-angular/src/lib/grid/container.component.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { booleanAttribute, Component, computed, input, InputSignal, InputSignalWithTransform } from '@angular/core';
2-
3-
import { IContainer } from './container.type';
1+
import { booleanAttribute, Component, computed, input } from '@angular/core';
42
import { Breakpoints } from '../coreui.types';
53

64
@Component({
@@ -9,19 +7,17 @@ import { Breakpoints } from '../coreui.types';
97
styleUrls: ['./container.component.scss'],
108
host: { '[class]': 'hostClasses()' }
119
})
12-
export class ContainerComponent implements IContainer {
10+
export class ContainerComponent {
1311
/**
1412
* Set container 100% wide until a breakpoint.
1513
*/
16-
readonly breakpoint: InputSignal<Exclude<Breakpoints, 'xs'>> = input<Exclude<Breakpoints, 'xs'>>('');
14+
readonly breakpoint = input<Exclude<Breakpoints, 'xs'>>('');
1715

1816
/**
1917
* Set container 100% wide, spanning the entire width of the viewport.
20-
* @type InputSignalWithTransform<unknown, boolean>
18+
* @return boolean
2119
*/
22-
readonly fluid: InputSignalWithTransform<unknown, boolean> = input<unknown, boolean>(false, {
23-
transform: booleanAttribute
24-
});
20+
readonly fluid = input(false, { transform: booleanAttribute });
2521

2622
readonly hostClasses = computed(() => {
2723
const breakpoint = this.breakpoint();
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,45 @@
1+
import { Component, DebugElement } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { By } from '@angular/platform-browser';
14
import { GutterDirective } from './gutter.directive';
5+
import { GutterBreakpoints, Gutters, IGutterObject } from './gutter.type';
6+
7+
@Component({
8+
imports: [GutterDirective],
9+
template: '<div [gutter]="gutter"></div>'
10+
})
11+
export class TestComponent {
12+
gutter: IGutterObject | GutterBreakpoints | Gutters = 5;
13+
}
214

315
describe('GutterDirective', () => {
16+
let fixture: ComponentFixture<TestComponent>;
17+
let debugElement: DebugElement;
18+
19+
beforeEach(() => {
20+
TestBed.configureTestingModule({
21+
imports: [TestComponent]
22+
});
23+
fixture = TestBed.createComponent(TestComponent);
24+
debugElement = fixture.debugElement.query(By.directive(GutterDirective));
25+
fixture.detectChanges();
26+
});
27+
428
it('should create an instance', () => {
5-
const directive = new GutterDirective();
6-
expect(directive).toBeTruthy();
29+
TestBed.runInInjectionContext(() => {
30+
const directive = new GutterDirective();
31+
expect(directive).toBeTruthy();
32+
});
33+
});
34+
35+
it('should have css class', () => {
36+
expect(debugElement.nativeElement).toHaveClass('g-5');
37+
fixture.componentInstance.gutter = { gx: 2, gy: 1 };
38+
fixture.detectChanges();
39+
expect(debugElement.nativeElement).toHaveClass('gx-2');
40+
expect(debugElement.nativeElement).toHaveClass('gy-1');
41+
fixture.componentInstance.gutter = { md: { g: 3 } };
42+
fixture.detectChanges();
43+
expect(debugElement.nativeElement).toHaveClass('g-md-3');
744
});
845
});

0 commit comments

Comments
 (0)