Skip to content

Commit c074575

Browse files
mollykreisrajsite
andauthored
Create Angular mixins for fractional-width columns and groupable columns (#1587)
# Pull Request ## 🤨 Rationale Resolves #1211 In Angular, we didn't have a solution for the code duplication that came with multiple columns being fractional width and groupable. This PR creates mixins for fractional width columns and groupable columns to reduce the amount of duplication. There is still the potential for future code reduction within the tests, but I decided that coming up to a solution single-sourcing more of our test code was out of scope for this PR. ## 👩‍💻 Implementation - Create `mixinFractionalWidthColumnAPI` function that allows a `NimbleTableColumnBaseDirective` to claim support for being fractional width (i.e. inputs for `fractionalWidth` and `minPixelWidth`). This function is exported from the existing `@ni/nimble-angular/table-column` entry point. - Create `mixinGroupableColumnAPI` function that allows a `NimbleTableColumnBaseDirective` to claim support for being groupable (i.e. inputs for `groupIndex` and `groupingDisabled`). This function is exported from the existing `@ni/nimble-angular/table-column` entry point. - Update existing table columns to use these new mixins - These changes required making `renderer` and `elementRef` public (though marked as internal) in `NimbleTableColumnBaseDirective` because otherwise TS error 4094 is triggered. This is related to the `mixinFractionalWidthColumnAPI` and `mixinGroupableColumnAPI` functions having an implicit return type and that inferred type including `NimbleTableColumnBaseDirective`. [The scenario I was hitting is described in this TypeScript issue](microsoft/TypeScript#30355). ## 🧪 Testing - Verified existing unit tests are passing - Verified Angular example app still works as expected ## ✅ Checklist <!--- Review the list and put an x in the boxes that apply or ~~strike through~~ around items that don't (along with an explanation). --> - [ ] I have updated the project documentation to reflect my changes or determined no changes are needed. --------- Co-authored-by: Milan Raj <[email protected]>
1 parent f5679e6 commit c074575

11 files changed

+122
-265
lines changed

angular-workspace/projects/ni/nimble-angular/table-column/anchor/nimble-table-column-anchor.directive.ts

+3-43
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
22
import { type TableColumnAnchor, tableColumnAnchorTag } from '@ni/nimble-components/dist/esm/table-column/anchor';
33
import { AnchorAppearance } from '@ni/nimble-components/dist/esm/anchor/types';
4-
import { BooleanValueOrAttribute, NumberValueOrAttribute, toBooleanProperty, toNullableNumberProperty } from '@ni/nimble-angular/internal-utilities';
5-
import { NimbleTableColumnBaseDirective } from '@ni/nimble-angular/table-column';
4+
import { BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities';
5+
import { NimbleTableColumnBaseDirective, mixinFractionalWidthColumnAPI, mixinGroupableColumnAPI } from '@ni/nimble-angular/table-column';
66

77
export type { TableColumnAnchor };
88
export { tableColumnAnchorTag };
@@ -14,7 +14,7 @@ export { AnchorAppearance };
1414
@Directive({
1515
selector: 'nimble-table-column-anchor'
1616
})
17-
export class NimbleTableColumnAnchorDirective extends NimbleTableColumnBaseDirective<TableColumnAnchor> {
17+
export class NimbleTableColumnAnchorDirective extends mixinFractionalWidthColumnAPI(mixinGroupableColumnAPI(NimbleTableColumnBaseDirective<TableColumnAnchor>)) {
1818
public get labelFieldName(): string | undefined {
1919
return this.elementRef.nativeElement.labelFieldName;
2020
}
@@ -109,46 +109,6 @@ export class NimbleTableColumnAnchorDirective extends NimbleTableColumnBaseDirec
109109
this.renderer.setProperty(this.elementRef.nativeElement, 'download', value);
110110
}
111111

112-
public get fractionalWidth(): number | null | undefined {
113-
return this.elementRef.nativeElement.fractionalWidth;
114-
}
115-
116-
// Renaming because property should have camel casing, but attribute should not
117-
// eslint-disable-next-line @angular-eslint/no-input-rename
118-
@Input('fractional-width') public set fractionalWidth(value: NumberValueOrAttribute | null | undefined) {
119-
this.renderer.setProperty(this.elementRef.nativeElement, 'fractionalWidth', toNullableNumberProperty(value));
120-
}
121-
122-
public get minPixelWidth(): number | null | undefined {
123-
return this.elementRef.nativeElement.minPixelWidth;
124-
}
125-
126-
// Renaming because property should have camel casing, but attribute should not
127-
// eslint-disable-next-line @angular-eslint/no-input-rename
128-
@Input('min-pixel-width') public set minPixelWidth(value: NumberValueOrAttribute | null | undefined) {
129-
this.renderer.setProperty(this.elementRef.nativeElement, 'minPixelWidth', toNullableNumberProperty(value));
130-
}
131-
132-
public get groupIndex(): number | null | undefined {
133-
return this.elementRef.nativeElement.groupIndex;
134-
}
135-
136-
// Renaming because property should have camel casing, but attribute should not
137-
// eslint-disable-next-line @angular-eslint/no-input-rename
138-
@Input('group-index') public set groupIndex(value: NumberValueOrAttribute | null | undefined) {
139-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupIndex', toNullableNumberProperty(value));
140-
}
141-
142-
public get groupingDisabled(): boolean {
143-
return this.elementRef.nativeElement.groupingDisabled;
144-
}
145-
146-
// Renaming because property should have camel casing, but attribute should not
147-
// eslint-disable-next-line @angular-eslint/no-input-rename
148-
@Input('grouping-disabled') public set groupingDisabled(value: BooleanValueOrAttribute) {
149-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupingDisabled', toBooleanProperty(value));
150-
}
151-
152112
public constructor(renderer: Renderer2, elementRef: ElementRef<TableColumnAnchor>) {
153113
super(renderer, elementRef);
154114
}

angular-workspace/projects/ni/nimble-angular/table-column/date-text/nimble-table-column-date-text.directive.ts

+2-43
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
22
import { type TableColumnDateText, tableColumnDateTextTag } from '@ni/nimble-components/dist/esm/table-column/date-text';
3-
import { BooleanValueOrAttribute, NumberValueOrAttribute, toBooleanProperty, toNullableNumberProperty } from '@ni/nimble-angular/internal-utilities';
4-
import { NimbleTableColumnBaseDirective } from '@ni/nimble-angular/table-column';
3+
import { NimbleTableColumnBaseDirective, mixinFractionalWidthColumnAPI, mixinGroupableColumnAPI } from '@ni/nimble-angular/table-column';
54
import {
65
DateTextFormat,
76
DateStyle,
@@ -46,7 +45,7 @@ export { tableColumnDateTextTag };
4645
@Directive({
4746
selector: 'nimble-table-column-date-text'
4847
})
49-
export class NimbleTableColumnDateTextDirective extends NimbleTableColumnBaseDirective<TableColumnDateText> {
48+
export class NimbleTableColumnDateTextDirective extends mixinFractionalWidthColumnAPI(mixinGroupableColumnAPI(NimbleTableColumnBaseDirective<TableColumnDateText>)) {
5049
public get fieldName(): string | undefined {
5150
return this.elementRef.nativeElement.fieldName;
5251
}
@@ -263,46 +262,6 @@ export class NimbleTableColumnDateTextDirective extends NimbleTableColumnBaseDir
263262
this.renderer.setProperty(this.elementRef.nativeElement, 'customHourCycle', value);
264263
}
265264

266-
public get fractionalWidth(): number | null | undefined {
267-
return this.elementRef.nativeElement.fractionalWidth;
268-
}
269-
270-
// Renaming because property should have camel casing, but attribute should not
271-
// eslint-disable-next-line @angular-eslint/no-input-rename
272-
@Input('fractional-width') public set fractionalWidth(value: NumberValueOrAttribute | null | undefined) {
273-
this.renderer.setProperty(this.elementRef.nativeElement, 'fractionalWidth', toNullableNumberProperty(value));
274-
}
275-
276-
public get minPixelWidth(): number | null | undefined {
277-
return this.elementRef.nativeElement.minPixelWidth;
278-
}
279-
280-
// Renaming because property should have camel casing, but attribute should not
281-
// eslint-disable-next-line @angular-eslint/no-input-rename
282-
@Input('min-pixel-width') public set minPixelWidth(value: NumberValueOrAttribute | null | undefined) {
283-
this.renderer.setProperty(this.elementRef.nativeElement, 'minPixelWidth', toNullableNumberProperty(value));
284-
}
285-
286-
public get groupIndex(): number | null | undefined {
287-
return this.elementRef.nativeElement.groupIndex;
288-
}
289-
290-
// Renaming because property should have camel casing, but attribute should not
291-
// eslint-disable-next-line @angular-eslint/no-input-rename
292-
@Input('group-index') public set groupIndex(value: NumberValueOrAttribute | null | undefined) {
293-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupIndex', toNullableNumberProperty(value));
294-
}
295-
296-
public get groupingDisabled(): boolean {
297-
return this.elementRef.nativeElement.groupingDisabled;
298-
}
299-
300-
// Renaming because property should have camel casing, but attribute should not
301-
// eslint-disable-next-line @angular-eslint/no-input-rename
302-
@Input('grouping-disabled') public set groupingDisabled(value: BooleanValueOrAttribute) {
303-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupingDisabled', toBooleanProperty(value));
304-
}
305-
306265
public constructor(renderer: Renderer2, elementRef: ElementRef<TableColumnDateText>) {
307266
super(renderer, elementRef);
308267
}

angular-workspace/projects/ni/nimble-angular/table-column/enum-text/nimble-table-column-enum-text.directive.ts

+2-43
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
22
import { type TableColumnEnumText, tableColumnEnumTextTag } from '@ni/nimble-components/dist/esm/table-column/enum-text';
3-
import { BooleanValueOrAttribute, NumberValueOrAttribute, toBooleanProperty, toNullableNumberProperty } from '@ni/nimble-angular/internal-utilities';
4-
import { NimbleTableColumnBaseDirective } from '@ni/nimble-angular/table-column';
3+
import { NimbleTableColumnBaseDirective, mixinFractionalWidthColumnAPI, mixinGroupableColumnAPI } from '@ni/nimble-angular/table-column';
54
import { MappingKeyType } from '@ni/nimble-components/dist/esm/table-column/enum-base/types';
65

76
export { MappingKeyType };
@@ -14,7 +13,7 @@ export { tableColumnEnumTextTag };
1413
@Directive({
1514
selector: 'nimble-table-column-enum-text'
1615
})
17-
export class NimbleTableColumnEnumTextDirective extends NimbleTableColumnBaseDirective<TableColumnEnumText> {
16+
export class NimbleTableColumnEnumTextDirective extends mixinFractionalWidthColumnAPI(mixinGroupableColumnAPI(NimbleTableColumnBaseDirective<TableColumnEnumText>)) {
1817
public get fieldName(): string | undefined {
1918
return this.elementRef.nativeElement.fieldName;
2019
}
@@ -35,46 +34,6 @@ export class NimbleTableColumnEnumTextDirective extends NimbleTableColumnBaseDir
3534
this.renderer.setProperty(this.elementRef.nativeElement, 'keyType', value);
3635
}
3736

38-
public get fractionalWidth(): number | null | undefined {
39-
return this.elementRef.nativeElement.fractionalWidth;
40-
}
41-
42-
// Renaming because property should have camel casing, but attribute should not
43-
// eslint-disable-next-line @angular-eslint/no-input-rename
44-
@Input('fractional-width') public set fractionalWidth(value: NumberValueOrAttribute | null | undefined) {
45-
this.renderer.setProperty(this.elementRef.nativeElement, 'fractionalWidth', toNullableNumberProperty(value));
46-
}
47-
48-
public get minPixelWidth(): number | null | undefined {
49-
return this.elementRef.nativeElement.minPixelWidth;
50-
}
51-
52-
// Renaming because property should have camel casing, but attribute should not
53-
// eslint-disable-next-line @angular-eslint/no-input-rename
54-
@Input('min-pixel-width') public set minPixelWidth(value: NumberValueOrAttribute | null | undefined) {
55-
this.renderer.setProperty(this.elementRef.nativeElement, 'minPixelWidth', toNullableNumberProperty(value));
56-
}
57-
58-
public get groupIndex(): number | null | undefined {
59-
return this.elementRef.nativeElement.groupIndex;
60-
}
61-
62-
// Renaming because property should have camel casing, but attribute should not
63-
// eslint-disable-next-line @angular-eslint/no-input-rename
64-
@Input('group-index') public set groupIndex(value: NumberValueOrAttribute | null | undefined) {
65-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupIndex', toNullableNumberProperty(value));
66-
}
67-
68-
public get groupingDisabled(): boolean {
69-
return this.elementRef.nativeElement.groupingDisabled;
70-
}
71-
72-
// Renaming because property should have camel casing, but attribute should not
73-
// eslint-disable-next-line @angular-eslint/no-input-rename
74-
@Input('grouping-disabled') public set groupingDisabled(value: BooleanValueOrAttribute) {
75-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupingDisabled', toBooleanProperty(value));
76-
}
77-
7837
public constructor(renderer: Renderer2, elementRef: ElementRef<TableColumnEnumText>) {
7938
super(renderer, elementRef);
8039
}

angular-workspace/projects/ni/nimble-angular/table-column/icon/nimble-table-column-icon.directive.ts

+2-43
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
22
import { type TableColumnIcon, tableColumnIconTag } from '@ni/nimble-components/dist/esm/table-column/icon';
3-
import { BooleanValueOrAttribute, NumberValueOrAttribute, toBooleanProperty, toNullableNumberProperty } from '@ni/nimble-angular/internal-utilities';
4-
import { NimbleTableColumnBaseDirective } from '@ni/nimble-angular/table-column';
3+
import { NimbleTableColumnBaseDirective, mixinFractionalWidthColumnAPI, mixinGroupableColumnAPI } from '@ni/nimble-angular/table-column';
54
import type { MappingKeyType } from '@ni/nimble-components/dist/esm/table-column/enum-base/types';
65

76
export type { TableColumnIcon };
@@ -13,7 +12,7 @@ export { tableColumnIconTag };
1312
@Directive({
1413
selector: 'nimble-table-column-icon'
1514
})
16-
export class NimbleTableColumnIconDirective extends NimbleTableColumnBaseDirective<TableColumnIcon> {
15+
export class NimbleTableColumnIconDirective extends mixinFractionalWidthColumnAPI(mixinGroupableColumnAPI(NimbleTableColumnBaseDirective<TableColumnIcon>)) {
1716
public get fieldName(): string | undefined {
1817
return this.elementRef.nativeElement.fieldName;
1918
}
@@ -34,46 +33,6 @@ export class NimbleTableColumnIconDirective extends NimbleTableColumnBaseDirecti
3433
this.renderer.setProperty(this.elementRef.nativeElement, 'keyType', value);
3534
}
3635

37-
public get fractionalWidth(): number | null | undefined {
38-
return this.elementRef.nativeElement.fractionalWidth;
39-
}
40-
41-
// Renaming because property should have camel casing, but attribute should not
42-
// eslint-disable-next-line @angular-eslint/no-input-rename
43-
@Input('fractional-width') public set fractionalWidth(value: NumberValueOrAttribute | null | undefined) {
44-
this.renderer.setProperty(this.elementRef.nativeElement, 'fractionalWidth', toNullableNumberProperty(value));
45-
}
46-
47-
public get minPixelWidth(): number | null | undefined {
48-
return this.elementRef.nativeElement.minPixelWidth;
49-
}
50-
51-
// Renaming because property should have camel casing, but attribute should not
52-
// eslint-disable-next-line @angular-eslint/no-input-rename
53-
@Input('min-pixel-width') public set minPixelWidth(value: NumberValueOrAttribute | null | undefined) {
54-
this.renderer.setProperty(this.elementRef.nativeElement, 'minPixelWidth', toNullableNumberProperty(value));
55-
}
56-
57-
public get groupIndex(): number | null | undefined {
58-
return this.elementRef.nativeElement.groupIndex;
59-
}
60-
61-
// Renaming because property should have camel casing, but attribute should not
62-
// eslint-disable-next-line @angular-eslint/no-input-rename
63-
@Input('group-index') public set groupIndex(value: NumberValueOrAttribute | null | undefined) {
64-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupIndex', toNullableNumberProperty(value));
65-
}
66-
67-
public get groupingDisabled(): boolean {
68-
return this.elementRef.nativeElement.groupingDisabled;
69-
}
70-
71-
// Renaming because property should have camel casing, but attribute should not
72-
// eslint-disable-next-line @angular-eslint/no-input-rename
73-
@Input('grouping-disabled') public set groupingDisabled(value: BooleanValueOrAttribute) {
74-
this.renderer.setProperty(this.elementRef.nativeElement, 'groupingDisabled', toBooleanProperty(value));
75-
}
76-
7736
public constructor(renderer: Renderer2, elementRef: ElementRef<TableColumnIcon>) {
7837
super(renderer, elementRef);
7938
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Directive, Input } from '@angular/core';
2+
import { NumberValueOrAttribute, toNullableNumberProperty } from '@ni/nimble-angular/internal-utilities';
3+
import type { TableColumn } from '@ni/nimble-components/dist/esm/table-column/base';
4+
import type { NimbleTableColumnBaseDirective } from '../nimble-table-column-base.directive';
5+
6+
type FractionalWidthColumn = TableColumn & {
7+
fractionalWidth?: number | null,
8+
minPixelWidth?: number | null
9+
};
10+
11+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12+
type FractionalWidthColumnDirectiveConstructor<T extends FractionalWidthColumn> = abstract new (...args: any[]) => NimbleTableColumnBaseDirective<T>;
13+
14+
// As the returned class is internal to the function, we can't write a signature that uses is directly, so rely on inference
15+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type
16+
export function mixinFractionalWidthColumnAPI<TBase extends FractionalWidthColumnDirectiveConstructor<FractionalWidthColumn>>(base: TBase) {
17+
/**
18+
* The Mixin that provides a concrete column directive with the API to support being resized
19+
* proportionally within a Table.
20+
*/
21+
@Directive()
22+
abstract class FractionalWidthColumnDirective extends base {
23+
public get fractionalWidth(): number | null | undefined {
24+
return this.elementRef.nativeElement.fractionalWidth;
25+
}
26+
27+
// Renaming because property should have camel casing, but attribute should not
28+
// eslint-disable-next-line @angular-eslint/no-input-rename
29+
@Input('fractional-width') public set fractionalWidth(value: NumberValueOrAttribute | null | undefined) {
30+
this.renderer.setProperty(this.elementRef.nativeElement, 'fractionalWidth', toNullableNumberProperty(value));
31+
}
32+
33+
public get minPixelWidth(): number | null | undefined {
34+
return this.elementRef.nativeElement.minPixelWidth;
35+
}
36+
37+
// Renaming because property should have camel casing, but attribute should not
38+
// eslint-disable-next-line @angular-eslint/no-input-rename
39+
@Input('min-pixel-width') public set minPixelWidth(value: NumberValueOrAttribute | null | undefined) {
40+
this.renderer.setProperty(this.elementRef.nativeElement, 'minPixelWidth', toNullableNumberProperty(value));
41+
}
42+
}
43+
return FractionalWidthColumnDirective;
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Directive, Input } from '@angular/core';
2+
import { BooleanValueOrAttribute, NumberValueOrAttribute, toBooleanProperty, toNullableNumberProperty } from '@ni/nimble-angular/internal-utilities';
3+
import type { TableColumn } from '@ni/nimble-components/dist/esm/table-column/base';
4+
import type { NimbleTableColumnBaseDirective } from '../nimble-table-column-base.directive';
5+
6+
type GroupableColumn = TableColumn & {
7+
groupingDisabled: boolean,
8+
groupIndex?: number | null
9+
};
10+
11+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12+
type GroupableColumnDirectiveConstructor<T extends GroupableColumn> = abstract new (...args: any[]) => NimbleTableColumnBaseDirective<T>;
13+
14+
// As the returned class is internal to the function, we can't write a signature that uses is directly, so rely on inference
15+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type
16+
export function mixinGroupableColumnAPI<TBase extends GroupableColumnDirectiveConstructor<GroupableColumn>>(base: TBase) {
17+
/**
18+
* The Mixin that provides a concrete column directive with the API to allow grouping
19+
* by the values in that column.
20+
*/
21+
@Directive()
22+
abstract class GroupableColumnDirective extends base {
23+
public get groupIndex(): number | null | undefined {
24+
return this.elementRef.nativeElement.groupIndex;
25+
}
26+
27+
// Renaming because property should have camel casing, but attribute should not
28+
// eslint-disable-next-line @angular-eslint/no-input-rename
29+
@Input('group-index') public set groupIndex(value: NumberValueOrAttribute | null | undefined) {
30+
this.renderer.setProperty(this.elementRef.nativeElement, 'groupIndex', toNullableNumberProperty(value));
31+
}
32+
33+
public get groupingDisabled(): boolean {
34+
return this.elementRef.nativeElement.groupingDisabled;
35+
}
36+
37+
// Renaming because property should have camel casing, but attribute should not
38+
// eslint-disable-next-line @angular-eslint/no-input-rename
39+
@Input('grouping-disabled') public set groupingDisabled(value: BooleanValueOrAttribute) {
40+
this.renderer.setProperty(this.elementRef.nativeElement, 'groupingDisabled', toBooleanProperty(value));
41+
}
42+
}
43+
return GroupableColumnDirective;
44+
}

angular-workspace/projects/ni/nimble-angular/table-column/nimble-table-column-base.directive.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,15 @@ export class NimbleTableColumnBaseDirective<T extends TableColumn> {
7171
this.renderer.setProperty(this.elementRef.nativeElement, 'sortIndex', toNullableNumberProperty(value));
7272
}
7373

74-
public constructor(protected readonly renderer: Renderer2, protected readonly elementRef: ElementRef<T>) {}
74+
/** @internal */
75+
public readonly renderer: Renderer2;
76+
/** @internal */
77+
public readonly elementRef: ElementRef<T>;
78+
79+
public constructor(renderer: Renderer2, elementRef: ElementRef<T>) {
80+
this.renderer = renderer;
81+
this.elementRef = elementRef;
82+
}
7583

7684
public checkValidity(): boolean {
7785
return this.elementRef.nativeElement.checkValidity();

0 commit comments

Comments
 (0)