diff --git a/angular-workspace/projects/example-client-app/src/app/app.module.ts b/angular-workspace/projects/example-client-app/src/app/app.module.ts index f63269e4f5..45721b321b 100644 --- a/angular-workspace/projects/example-client-app/src/app/app.module.ts +++ b/angular-workspace/projects/example-client-app/src/app/app.module.ts @@ -13,11 +13,14 @@ import { NimbleTextAreaModule, NimbleTextFieldModule, NimbleNumberFieldModule, N import { NimbleLabelProviderCoreModule } from '@ni/nimble-angular/label-provider/core'; import { NimbleLabelProviderTableModule } from '@ni/nimble-angular/label-provider/table'; import { NimbleMappingTextModule } from '@ni/nimble-angular/mapping/text'; +import { NimbleMappingIconModule } from '@ni/nimble-angular/mapping/icon'; +import { NimbleMappingSpinnerModule } from '@ni/nimble-angular/mapping/spinner'; import { NimbleTableModule } from '@ni/nimble-angular/table'; import { NimbleTableColumnTextModule } from '@ni/nimble-angular/table-column/text'; import { NimbleTableColumnAnchorModule } from '@ni/nimble-angular/table-column/anchor'; import { NimbleTableColumnDateTextModule } from '@ni/nimble-angular/table-column/date-text'; import { NimbleTableColumnEnumTextModule } from '@ni/nimble-angular/table-column/enum-text'; +import { NimbleTableColumnIconModule } from '@ni/nimble-angular/table-column/icon'; import { NimbleRichTextViewerModule } from '@ni/nimble-angular/rich-text-viewer'; import { AppComponent } from './app.component'; import { CustomAppComponent } from './customapp/customapp.component'; @@ -81,6 +84,9 @@ import { HeaderComponent } from './header/header.component'; NimbleMappingTextModule, NimbleBannerModule, NimbleRichTextViewerModule, + NimbleTableColumnIconModule, + NimbleMappingIconModule, + NimbleMappingSpinnerModule, RouterModule.forRoot( [ { path: '', redirectTo: '/customapp', pathMatch: 'full' }, diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html index d7c0a5654a..8f42865860 100644 --- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html +++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html @@ -222,6 +222,22 @@ Status + + + + + + Result + String 2 diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts index 9687744301..d39d1b18fc 100644 --- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts +++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts @@ -18,6 +18,7 @@ interface SimpleTableRecord extends TableRecord { linkLabel?: string; date: number; statusCode: number; + result: string; } @Component({ @@ -109,7 +110,8 @@ export class CustomAppComponent { href: '/customapp', linkLabel: 'Link', date: (tableData.length % 2 === 0) ? new Date(2023, 7, 16, 3, 56, 11).valueOf() : new Date(2022, 2, 7, 20, 28, 41).valueOf(), - statusCode: (tableData.length % 2 === 0) ? 100 : 101 + statusCode: (tableData.length % 2 === 0) ? 100 : 101, + result: (tableData.length % 2 === 0) ? 'success' : 'unknown' }); this.tableDataSubject.next(tableData); } diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/icon/ng-package.json b/angular-workspace/projects/ni/nimble-angular/mapping/icon/ng-package.json new file mode 100644 index 0000000000..e5440110fb --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/icon/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/icon/nimble-mapping-icon.directive.ts b/angular-workspace/projects/ni/nimble-angular/mapping/icon/nimble-mapping-icon.directive.ts new file mode 100644 index 0000000000..d37ea1c02e --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/icon/nimble-mapping-icon.directive.ts @@ -0,0 +1,49 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import { type MappingIcon, mappingIconTag } from '@ni/nimble-components/dist/esm/mapping/icon'; +import type { MappingKey } from '@ni/nimble-components/dist/esm/mapping/base/types'; +import type { IconSeverity } from '@ni/nimble-components/dist/esm/icon-base/types'; + +export type { MappingIcon }; +export { mappingIconTag }; + +/** + * Directive to provide Angular integration for the mapping icon element used by the icon column. + */ +@Directive({ + selector: 'nimble-mapping-icon' +}) +export class NimbleMappingIconDirective { + public get key(): MappingKey | undefined { + return this.elementRef.nativeElement.key; + } + + @Input() public set key(value: MappingKey | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'key', value); + } + + public get text(): string | undefined { + return this.elementRef.nativeElement.text; + } + + @Input() public set text(value: string | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'text', value); + } + + public get icon(): string | undefined { + return this.elementRef.nativeElement.icon; + } + + @Input() public set icon(value: string | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'icon', value); + } + + public get severity(): IconSeverity { + return this.elementRef.nativeElement.severity; + } + + @Input() public set severity(value: IconSeverity) { + this.renderer.setProperty(this.elementRef.nativeElement, 'severity', value); + } + + public constructor(protected readonly renderer: Renderer2, protected readonly elementRef: ElementRef) {} +} \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/icon/nimble-mapping-icon.module.ts b/angular-workspace/projects/ni/nimble-angular/mapping/icon/nimble-mapping-icon.module.ts new file mode 100644 index 0000000000..ede6667b5b --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/icon/nimble-mapping-icon.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NimbleMappingIconDirective } from './nimble-mapping-icon.directive'; + +import '@ni/nimble-components/dist/esm/mapping/icon'; + +@NgModule({ + declarations: [NimbleMappingIconDirective], + imports: [CommonModule], + exports: [NimbleMappingIconDirective] +}) +export class NimbleMappingIconModule { } \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/icon/public-api.ts b/angular-workspace/projects/ni/nimble-angular/mapping/icon/public-api.ts new file mode 100644 index 0000000000..d0b64e72ff --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/icon/public-api.ts @@ -0,0 +1,2 @@ +export * from './nimble-mapping-icon.directive'; +export * from './nimble-mapping-icon.module'; \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/icon/tests/nimble-mapping-icon.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/mapping/icon/tests/nimble-mapping-icon.directive.spec.ts new file mode 100644 index 0000000000..8ad4f354e3 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/icon/tests/nimble-mapping-icon.directive.spec.ts @@ -0,0 +1,251 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { IconSeverity } from '@ni/nimble-angular'; +import { NimbleTableModule } from '../../../table/nimble-table.module'; +import { NimbleTableColumnIconModule } from '../../../table-column/icon/nimble-table-column-icon.module'; +import { NimbleMappingIconDirective, type MappingIcon } from '../nimble-mapping-icon.directive'; +import { NimbleMappingIconModule } from '../nimble-mapping-icon.module'; + +describe('NimbleMappingIcon', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NimbleMappingIconModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('nimble-mapping-text')).not.toBeUndefined(); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + + + + + ` + }) + class TestHostComponent { + @ViewChild('mapping', { read: NimbleMappingIconDirective }) public directive: NimbleMappingIconDirective; + @ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: NimbleMappingIconDirective; + let nativeElement: MappingIcon; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleMappingIconModule, NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for key', () => { + expect(directive.key).toBe('false'); + expect(nativeElement.key).toBe('false'); + }); + + it('will use template string values for text', () => { + expect(directive.text).toBe('nope'); + expect(nativeElement.text).toBe('nope'); + }); + + it('will use template string values for icon', () => { + expect(directive.icon).toBe('nimble-icon-xmark'); + expect(nativeElement.icon).toBe('nimble-icon-xmark'); + }); + + it('will use template string values for severity', () => { + expect(directive.severity).toBe('error'); + expect(nativeElement.severity).toBe('error'); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + + + + + ` + }) + class TestHostComponent { + @ViewChild('mapping', { read: NimbleMappingIconDirective }) public directive: NimbleMappingIconDirective; + @ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef; + public key = false; + public text = 'nope'; + public icon = 'nimble-icon-xmark'; + public severity: IconSeverity = IconSeverity.error; + } + + let fixture: ComponentFixture; + let directive: NimbleMappingIconDirective; + let nativeElement: MappingIcon; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleMappingIconModule, NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for key', () => { + expect(directive.key).toBeFalse(); + expect(nativeElement.key).toBeFalse(); + + fixture.componentInstance.key = true; + fixture.detectChanges(); + + expect(directive.key).toBeTrue(); + expect(nativeElement.key).toBeTrue(); + }); + + it('can be configured with property binding for text', () => { + expect(directive.text).toBe('nope'); + expect(nativeElement.text).toBe('nope'); + + fixture.componentInstance.text = 'yep'; + fixture.detectChanges(); + + expect(directive.text).toBe('yep'); + expect(nativeElement.text).toBe('yep'); + }); + + it('can be configured with property binding for icon', () => { + expect(directive.icon).toBe('nimble-icon-xmark'); + expect(nativeElement.icon).toBe('nimble-icon-xmark'); + + fixture.componentInstance.icon = 'nimble-icon-check'; + fixture.detectChanges(); + + expect(directive.icon).toBe('nimble-icon-check'); + expect(nativeElement.icon).toBe('nimble-icon-check'); + }); + + it('can be configured with property binding for severity', () => { + expect(directive.severity).toBe('error'); + expect(nativeElement.severity).toBe('error'); + + fixture.componentInstance.severity = IconSeverity.success; + fixture.detectChanges(); + + expect(directive.severity).toBe('success'); + expect(nativeElement.severity).toBe('success'); + }); + }); + + describe('with attribute bound values', () => { + @Component({ + template: ` + + + + + + + ` + }) + class TestHostComponent { + @ViewChild('mapping', { read: NimbleMappingIconDirective }) public directive: NimbleMappingIconDirective; + @ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef; + public key = false; + public text = 'nope'; + public icon = 'nimble-icon-xmark'; + public severity: IconSeverity = IconSeverity.error; + } + + let fixture: ComponentFixture; + let directive: NimbleMappingIconDirective; + let nativeElement: MappingIcon; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleMappingIconModule, NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with attribute binding for key', () => { + expect(directive.key).toBe('false'); + expect(nativeElement.key).toBe('false'); + + fixture.componentInstance.key = true; + fixture.detectChanges(); + + expect(directive.key).toBe('true'); + expect(nativeElement.key).toBe('true'); + }); + + it('can be configured with attribute binding for text', () => { + expect(directive.text).toBe('nope'); + expect(nativeElement.text).toBe('nope'); + + fixture.componentInstance.text = 'yep'; + fixture.detectChanges(); + + expect(directive.text).toBe('yep'); + expect(nativeElement.text).toBe('yep'); + }); + + it('can be configured with property binding for icon', () => { + expect(directive.icon).toBe('nimble-icon-xmark'); + expect(nativeElement.icon).toBe('nimble-icon-xmark'); + + fixture.componentInstance.icon = 'nimble-icon-check'; + fixture.detectChanges(); + + expect(directive.icon).toBe('nimble-icon-check'); + expect(nativeElement.icon).toBe('nimble-icon-check'); + }); + + it('can be configured with property binding for severity', () => { + expect(directive.severity).toBe('error'); + expect(nativeElement.severity).toBe('error'); + + fixture.componentInstance.severity = IconSeverity.success; + fixture.detectChanges(); + + expect(directive.severity).toBe('success'); + expect(nativeElement.severity).toBe('success'); + }); + }); +}); \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/spinner/ng-package.json b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/ng-package.json new file mode 100644 index 0000000000..e5440110fb --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/spinner/nimble-mapping-spinner.directive.ts b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/nimble-mapping-spinner.directive.ts new file mode 100644 index 0000000000..96de8801d2 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/nimble-mapping-spinner.directive.ts @@ -0,0 +1,32 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import { type MappingSpinner, mappingSpinnerTag } from '@ni/nimble-components/dist/esm/mapping/spinner'; +import type { MappingKey } from '@ni/nimble-components/dist/esm/mapping/base/types'; + +export type { MappingSpinner }; +export { mappingSpinnerTag }; + +/** + * Directive to provide Angular integration for the mapping spinner element used by the spinner column. + */ +@Directive({ + selector: 'nimble-mapping-spinner' +}) +export class NimbleMappingSpinnerDirective { + public get key(): MappingKey | undefined { + return this.elementRef.nativeElement.key; + } + + @Input() public set key(value: MappingKey | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'key', value); + } + + public get text(): string | undefined { + return this.elementRef.nativeElement.text; + } + + @Input() public set text(value: string | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'text', value); + } + + public constructor(protected readonly renderer: Renderer2, protected readonly elementRef: ElementRef) {} +} \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/spinner/nimble-mapping-spinner.module.ts b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/nimble-mapping-spinner.module.ts new file mode 100644 index 0000000000..fac1a07768 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/nimble-mapping-spinner.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NimbleMappingSpinnerDirective } from './nimble-mapping-spinner.directive'; + +import '@ni/nimble-components/dist/esm/mapping/spinner'; + +@NgModule({ + declarations: [NimbleMappingSpinnerDirective], + imports: [CommonModule], + exports: [NimbleMappingSpinnerDirective] +}) +export class NimbleMappingSpinnerModule { } \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/spinner/public-api.ts b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/public-api.ts new file mode 100644 index 0000000000..3ffc908c64 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/public-api.ts @@ -0,0 +1,2 @@ +export * from './nimble-mapping-spinner.directive'; +export * from './nimble-mapping-spinner.module'; \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/mapping/spinner/tests/nimble-mapping-spinner.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/tests/nimble-mapping-spinner.directive.spec.ts new file mode 100644 index 0000000000..d90ef54c60 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/mapping/spinner/tests/nimble-mapping-spinner.directive.spec.ts @@ -0,0 +1,186 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NimbleTableModule } from '../../../table/nimble-table.module'; +import { NimbleTableColumnIconModule } from '../../../table-column/icon/nimble-table-column-icon.module'; +import { NimbleMappingSpinnerDirective, type MappingSpinner } from '../nimble-mapping-spinner.directive'; +import { NimbleMappingSpinnerModule } from '../nimble-mapping-spinner.module'; + +describe('NimbleMappingSpinner', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NimbleMappingSpinnerModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('nimble-mapping-spinner')).not.toBeUndefined(); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + + + + + ` + }) + class TestHostComponent { + @ViewChild('mapping', { read: NimbleMappingSpinnerDirective }) public directive: NimbleMappingSpinnerDirective; + @ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: NimbleMappingSpinnerDirective; + let nativeElement: MappingSpinner; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleMappingSpinnerModule, NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for key', () => { + expect(directive.key).toBe('false'); + expect(nativeElement.key).toBe('false'); + }); + + it('will use template string values for text', () => { + expect(directive.text).toBe('nope'); + expect(nativeElement.text).toBe('nope'); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + + + + + ` + }) + class TestHostComponent { + @ViewChild('mapping', { read: NimbleMappingSpinnerDirective }) public directive: NimbleMappingSpinnerDirective; + @ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef; + public key = false; + public text = 'nope'; + } + + let fixture: ComponentFixture; + let directive: NimbleMappingSpinnerDirective; + let nativeElement: MappingSpinner; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleMappingSpinnerModule, NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for key', () => { + expect(directive.key).toBeFalse(); + expect(nativeElement.key).toBeFalse(); + + fixture.componentInstance.key = true; + fixture.detectChanges(); + + expect(directive.key).toBeTrue(); + expect(nativeElement.key).toBeTrue(); + }); + + it('can be configured with property binding for text', () => { + expect(directive.text).toBe('nope'); + expect(nativeElement.text).toBe('nope'); + + fixture.componentInstance.text = 'yep'; + fixture.detectChanges(); + + expect(directive.text).toBe('yep'); + expect(nativeElement.text).toBe('yep'); + }); + }); + + describe('with attribute bound values', () => { + @Component({ + template: ` + + + + + + + ` + }) + class TestHostComponent { + @ViewChild('mapping', { read: NimbleMappingSpinnerDirective }) public directive: NimbleMappingSpinnerDirective; + @ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef; + public key = false; + public text = 'nope'; + } + + let fixture: ComponentFixture; + let directive: NimbleMappingSpinnerDirective; + let nativeElement: MappingSpinner; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleMappingSpinnerModule, NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with attribute binding for key', () => { + expect(directive.key).toBe('false'); + expect(nativeElement.key).toBe('false'); + + fixture.componentInstance.key = true; + fixture.detectChanges(); + + expect(directive.key).toBe('true'); + expect(nativeElement.key).toBe('true'); + }); + + it('can be configured with attribute binding for text', () => { + expect(directive.text).toBe('nope'); + expect(nativeElement.text).toBe('nope'); + + fixture.componentInstance.text = 'yep'; + fixture.detectChanges(); + + expect(directive.text).toBe('yep'); + expect(nativeElement.text).toBe('yep'); + }); + }); +}); \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/table-column/icon/ng-package.json b/angular-workspace/projects/ni/nimble-angular/table-column/icon/ng-package.json new file mode 100644 index 0000000000..e5440110fb --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/table-column/icon/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/table-column/icon/nimble-table-column-icon.directive.ts b/angular-workspace/projects/ni/nimble-angular/table-column/icon/nimble-table-column-icon.directive.ts new file mode 100644 index 0000000000..e3f969ab36 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/table-column/icon/nimble-table-column-icon.directive.ts @@ -0,0 +1,80 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import { type TableColumnIcon, tableColumnIconTag } from '@ni/nimble-components/dist/esm/table-column/icon'; +import { BooleanValueOrAttribute, NumberValueOrAttribute, toBooleanProperty, toNullableNumberProperty } from '@ni/nimble-angular/internal-utilities'; +import { NimbleTableColumnBaseDirective } from '@ni/nimble-angular/table-column'; +import type { MappingKeyType } from '@ni/nimble-components/dist/esm/table-column/enum-base/types'; + +export type { TableColumnIcon }; +export { tableColumnIconTag }; + +/** + * Directive to provide Angular integration for the table column element for icons/spinners. + */ +@Directive({ + selector: 'nimble-table-column-icon' +}) +export class NimbleTableColumnIconDirective extends NimbleTableColumnBaseDirective { + public get fieldName(): string | undefined { + return this.elementRef.nativeElement.fieldName; + } + + // Renaming because property should have camel casing, but attribute should not + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('field-name') public set fieldName(value: string | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'fieldName', value); + } + + public get keyType(): MappingKeyType { + return this.elementRef.nativeElement.keyType; + } + + // Renaming because property should have camel casing, but attribute should not + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('key-type') public set keyType(value: MappingKeyType) { + this.renderer.setProperty(this.elementRef.nativeElement, 'keyType', value); + } + + public get fractionalWidth(): number | null | undefined { + return this.elementRef.nativeElement.fractionalWidth; + } + + // Renaming because property should have camel casing, but attribute should not + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('fractional-width') public set fractionalWidth(value: NumberValueOrAttribute | null | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'fractionalWidth', toNullableNumberProperty(value)); + } + + public get minPixelWidth(): number | null | undefined { + return this.elementRef.nativeElement.minPixelWidth; + } + + // Renaming because property should have camel casing, but attribute should not + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('min-pixel-width') public set minPixelWidth(value: NumberValueOrAttribute | null | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'minPixelWidth', toNullableNumberProperty(value)); + } + + public get groupIndex(): number | null | undefined { + return this.elementRef.nativeElement.groupIndex; + } + + // Renaming because property should have camel casing, but attribute should not + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('group-index') public set groupIndex(value: NumberValueOrAttribute | null | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'groupIndex', toNullableNumberProperty(value)); + } + + public get groupingDisabled(): boolean { + return this.elementRef.nativeElement.groupingDisabled; + } + + // Renaming because property should have camel casing, but attribute should not + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('grouping-disabled') public set groupingDisabled(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'groupingDisabled', toBooleanProperty(value)); + } + + public constructor(renderer: Renderer2, elementRef: ElementRef) { + super(renderer, elementRef); + } +} diff --git a/angular-workspace/projects/ni/nimble-angular/table-column/icon/nimble-table-column-icon.module.ts b/angular-workspace/projects/ni/nimble-angular/table-column/icon/nimble-table-column-icon.module.ts new file mode 100644 index 0000000000..d3d95db680 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/table-column/icon/nimble-table-column-icon.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NimbleTableColumnIconDirective } from './nimble-table-column-icon.directive'; + +import '@ni/nimble-components/dist/esm/table-column/icon'; + +@NgModule({ + declarations: [NimbleTableColumnIconDirective], + imports: [CommonModule], + exports: [NimbleTableColumnIconDirective] +}) +export class NimbleTableColumnIconModule { } diff --git a/angular-workspace/projects/ni/nimble-angular/table-column/icon/public-api.ts b/angular-workspace/projects/ni/nimble-angular/table-column/icon/public-api.ts new file mode 100644 index 0000000000..209eb764b2 --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/table-column/icon/public-api.ts @@ -0,0 +1,2 @@ +export * from './nimble-table-column-icon.directive'; +export * from './nimble-table-column-icon.module'; \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/table-column/icon/tests/nimble-table-column-icon.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/table-column/icon/tests/nimble-table-column-icon.directive.spec.ts new file mode 100644 index 0000000000..e060362d5f --- /dev/null +++ b/angular-workspace/projects/ni/nimble-angular/table-column/icon/tests/nimble-table-column-icon.directive.spec.ts @@ -0,0 +1,585 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NimbleTableModule } from '../../../table/nimble-table.module'; +import { NimbleTableColumnIconModule } from '../nimble-table-column-icon.module'; +import { NimbleTableColumnIconDirective, TableColumnIcon } from '../nimble-table-column-icon.directive'; +import { TableColumnSortDirection } from '../../nimble-table-column-base.directive'; + +describe('NimbleTableColumnIcon', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NimbleTableColumnIconModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('nimble-table-column-icon')).not.toBeUndefined(); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + + ` + }) + class TestHostComponent { + @ViewChild('column', { read: NimbleTableColumnIconDirective }) public directive: NimbleTableColumnIconDirective; + @ViewChild('column', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: NimbleTableColumnIconDirective; + let nativeElement: TableColumnIcon; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for fieldName', () => { + expect(directive.fieldName).toBe('field1'); + expect(nativeElement.fieldName).toBe('field1'); + }); + + it('will use template string values for keyType', () => { + expect(directive.keyType).toBe('boolean'); + expect(nativeElement.keyType).toBe('boolean'); + }); + + it('will use template string values for actionMenuSlot', () => { + expect(directive.actionMenuSlot).toBe('my-slot'); + expect(nativeElement.actionMenuSlot).toBe('my-slot'); + }); + + it('will use template string values for actionMenuLabel', () => { + expect(directive.actionMenuLabel).toBe('my menu'); + expect(nativeElement.actionMenuLabel).toBe('my menu'); + }); + + it('will use template string values for columnId', () => { + expect(directive.columnId).toBe('my-column'); + expect(nativeElement.columnId).toBe('my-column'); + }); + + it('will use template string value for columnHidden', () => { + expect(directive.columnHidden).toBe(true); + expect(nativeElement.columnHidden).toBe(true); + }); + + it('will use template string values for sortDirection', () => { + expect(directive.sortDirection).toBe(TableColumnSortDirection.ascending); + expect(nativeElement.sortDirection).toBe(TableColumnSortDirection.ascending); + }); + + it('will use template string value for sortIndex', () => { + expect(directive.sortIndex).toBe(0); + expect(nativeElement.sortIndex).toBe(0); + }); + + it('will use template string values for fractionalWidth', () => { + expect(directive.fractionalWidth).toBe(2); + expect(nativeElement.fractionalWidth).toBe(2); + }); + + it('will use template string values for minPixelWidth', () => { + expect(directive.minPixelWidth).toBe(40); + expect(nativeElement.minPixelWidth).toBe(40); + }); + + it('will use template string values for groupIndex', () => { + expect(directive.groupIndex).toBe(0); + expect(nativeElement.groupIndex).toBe(0); + }); + + it('will use template string values for groupingDisabled', () => { + expect(directive.groupingDisabled).toBeTrue(); + expect(nativeElement.groupingDisabled).toBeTrue(); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + + ` + }) + class TestHostComponent { + @ViewChild('column', { read: NimbleTableColumnIconDirective }) public directive: NimbleTableColumnIconDirective; + @ViewChild('column', { read: ElementRef }) public elementRef: ElementRef; + public field = 'field1'; + public keyType = 'boolean'; + public actionMenuSlot = 'my-slot'; + public actionMenuLabel = 'my menu'; + public fractionalWidth: number | null = 2; + public minPixelWidth: number | null = 40; + public columnId = 'my-column'; + public columnHidden = true; + public sortDirection: TableColumnSortDirection = TableColumnSortDirection.ascending; + public sortIndex: number | null = 0; + public groupIndex: number | null = 0; + public groupingDisabled = false; + } + + let fixture: ComponentFixture; + let directive: NimbleTableColumnIconDirective; + let nativeElement: TableColumnIcon; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for fieldName', () => { + expect(directive.fieldName).toBe('field1'); + expect(nativeElement.fieldName).toBe('field1'); + + fixture.componentInstance.field = 'field2'; + fixture.detectChanges(); + + expect(directive.fieldName).toBe('field2'); + expect(nativeElement.fieldName).toBe('field2'); + }); + + it('can be configured with property binding for keyType', () => { + expect(directive.keyType).toBe('boolean'); + expect(nativeElement.keyType).toBe('boolean'); + + fixture.componentInstance.keyType = 'number'; + fixture.detectChanges(); + + expect(directive.keyType).toBe('number'); + expect(nativeElement.keyType).toBe('number'); + }); + + it('can be configured with property binding for actionMenuSlot', () => { + expect(directive.actionMenuSlot).toBe('my-slot'); + expect(nativeElement.actionMenuSlot).toBe('my-slot'); + + fixture.componentInstance.actionMenuSlot = 'new-slot'; + fixture.detectChanges(); + + expect(directive.actionMenuSlot).toBe('new-slot'); + expect(nativeElement.actionMenuSlot).toBe('new-slot'); + }); + + it('can be configured with property binding for actionMenuLabel', () => { + expect(directive.actionMenuLabel).toBe('my menu'); + expect(nativeElement.actionMenuLabel).toBe('my menu'); + + fixture.componentInstance.actionMenuLabel = 'another menu'; + fixture.detectChanges(); + + expect(directive.actionMenuLabel).toBe('another menu'); + expect(nativeElement.actionMenuLabel).toBe('another menu'); + }); + + it('can be configured with property binding for columnId', () => { + expect(directive.columnId).toBe('my-column'); + expect(nativeElement.columnId).toBe('my-column'); + + fixture.componentInstance.columnId = 'new-column'; + fixture.detectChanges(); + + expect(directive.columnId).toBe('new-column'); + expect(nativeElement.columnId).toBe('new-column'); + }); + + it('can be configured with property binding for columnHidden', () => { + expect(directive.columnHidden).toBe(true); + expect(nativeElement.columnHidden).toBe(true); + + fixture.componentInstance.columnHidden = false; + fixture.detectChanges(); + + expect(directive.columnHidden).toBe(false); + expect(nativeElement.columnHidden).toBe(false); + }); + + it('can be configured with property binding for sortDirection', () => { + expect(directive.sortDirection).toBe(TableColumnSortDirection.ascending); + expect(nativeElement.sortDirection).toBe(TableColumnSortDirection.ascending); + + fixture.componentInstance.sortDirection = TableColumnSortDirection.descending; + fixture.detectChanges(); + + expect(directive.sortDirection).toBe(TableColumnSortDirection.descending); + expect(nativeElement.sortDirection).toBe(TableColumnSortDirection.descending); + }); + + it('can be configured with property binding for sortIndex', () => { + expect(directive.sortIndex).toBe(0); + expect(nativeElement.sortIndex).toBe(0); + + fixture.componentInstance.sortIndex = 1; + fixture.detectChanges(); + + expect(directive.sortIndex).toBe(1); + expect(nativeElement.sortIndex).toBe(1); + }); + + it('can be configured with property binding for sortIndex updated to null', () => { + expect(directive.sortIndex).toBe(0); + expect(nativeElement.sortIndex).toBe(0); + + fixture.componentInstance.sortIndex = null; + fixture.detectChanges(); + + expect(directive.sortIndex).toBe(null); + expect(nativeElement.sortIndex).toBe(null); + }); + + it('can be configured with property binding for fractionalWidth', () => { + expect(directive.fractionalWidth).toBe(2); + expect(nativeElement.fractionalWidth).toBe(2); + + fixture.componentInstance.fractionalWidth = 1; + fixture.detectChanges(); + + expect(directive.fractionalWidth).toBe(1); + expect(nativeElement.fractionalWidth).toBe(1); + }); + + it('can be configured with property binding for fractionalWidth updated to null', () => { + expect(directive.fractionalWidth).toBe(2); + expect(nativeElement.fractionalWidth).toBe(2); + + fixture.componentInstance.fractionalWidth = null; + fixture.detectChanges(); + + expect(directive.fractionalWidth).toBe(null); + expect(nativeElement.fractionalWidth).toBe(null); + }); + + it('can be configured with property binding for minPixelWidth', () => { + expect(directive.minPixelWidth).toBe(40); + expect(nativeElement.minPixelWidth).toBe(40); + + fixture.componentInstance.minPixelWidth = 50; + fixture.detectChanges(); + + expect(directive.minPixelWidth).toBe(50); + expect(nativeElement.minPixelWidth).toBe(50); + }); + + it('can be configured with property binding for minPixelWidth updated to null', () => { + expect(directive.minPixelWidth).toBe(40); + expect(nativeElement.minPixelWidth).toBe(40); + + fixture.componentInstance.minPixelWidth = null; + fixture.detectChanges(); + + expect(directive.minPixelWidth).toBe(null); + expect(nativeElement.minPixelWidth).toBe(null); + }); + + it('can be configured with property binding for groupIndex', () => { + expect(directive.groupIndex).toBe(0); + expect(nativeElement.groupIndex).toBe(0); + + fixture.componentInstance.groupIndex = 1; + fixture.detectChanges(); + + expect(directive.groupIndex).toBe(1); + expect(nativeElement.groupIndex).toBe(1); + }); + + it('can be configured with property binding for groupIndex updated to null', () => { + expect(directive.groupIndex).toBe(0); + expect(nativeElement.groupIndex).toBe(0); + + fixture.componentInstance.groupIndex = null; + fixture.detectChanges(); + + expect(directive.groupIndex).toBe(null); + expect(nativeElement.groupIndex).toBe(null); + }); + + it('can be configured with property binding for groupingDisabled', () => { + expect(directive.groupingDisabled).toBeFalse(); + expect(nativeElement.groupingDisabled).toBeFalse(); + + fixture.componentInstance.groupingDisabled = true; + fixture.detectChanges(); + + expect(directive.groupingDisabled).toBeTrue(); + expect(nativeElement.groupingDisabled).toBeTrue(); + }); + }); + + describe('with attribute bound values', () => { + @Component({ + template: ` + + + + ` + }) + class TestHostComponent { + @ViewChild('column', { read: NimbleTableColumnIconDirective }) public directive: NimbleTableColumnIconDirective; + @ViewChild('column', { read: ElementRef }) public elementRef: ElementRef; + public field = 'field1'; + public keyType = 'boolean'; + public actionMenuSlot = 'my-slot'; + public actionMenuLabel = 'my menu'; + public fractionalWidth: number | null = 2; + public minPixelWidth: number | null = 40; + public columnId = 'my-column'; + public columnHidden = true; + public sortDirection: TableColumnSortDirection = TableColumnSortDirection.ascending; + public sortIndex: number | null = 0; + public groupIndex: number | null = 0; + public groupingDisabled = false; + } + + let fixture: ComponentFixture; + let directive: NimbleTableColumnIconDirective; + let nativeElement: TableColumnIcon; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [NimbleTableColumnIconModule, NimbleTableModule] + }); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with attribute binding for fieldName', () => { + expect(directive.fieldName).toBe('field1'); + expect(nativeElement.fieldName).toBe('field1'); + + fixture.componentInstance.field = 'field2'; + fixture.detectChanges(); + + expect(directive.fieldName).toBe('field2'); + expect(nativeElement.fieldName).toBe('field2'); + }); + + it('can be configured with attribute binding for keyType', () => { + expect(directive.keyType).toBe('boolean'); + expect(nativeElement.keyType).toBe('boolean'); + + fixture.componentInstance.keyType = 'number'; + fixture.detectChanges(); + + expect(directive.keyType).toBe('number'); + expect(nativeElement.keyType).toBe('number'); + }); + + it('can be configured with attribute binding for actionMenuSlot', () => { + expect(directive.actionMenuSlot).toBe('my-slot'); + expect(nativeElement.actionMenuSlot).toBe('my-slot'); + + fixture.componentInstance.actionMenuSlot = 'new-slot'; + fixture.detectChanges(); + + expect(directive.actionMenuSlot).toBe('new-slot'); + expect(nativeElement.actionMenuSlot).toBe('new-slot'); + }); + + it('can be configured with attribute binding for actionMenuLabel', () => { + expect(directive.actionMenuLabel).toBe('my menu'); + expect(nativeElement.actionMenuLabel).toBe('my menu'); + + fixture.componentInstance.actionMenuLabel = 'another menu'; + fixture.detectChanges(); + + expect(directive.actionMenuLabel).toBe('another menu'); + expect(nativeElement.actionMenuLabel).toBe('another menu'); + }); + + it('can be configured with attribute binding for columnId', () => { + expect(directive.columnId).toBe('my-column'); + expect(nativeElement.columnId).toBe('my-column'); + + fixture.componentInstance.columnId = 'new-column'; + fixture.detectChanges(); + + expect(directive.columnId).toBe('new-column'); + expect(nativeElement.columnId).toBe('new-column'); + }); + + it('can be configured with attribute binding for columnHidden', () => { + expect(directive.columnHidden).toBe(true); + expect(nativeElement.columnHidden).toBe(true); + + fixture.componentInstance.columnHidden = false; + fixture.detectChanges(); + + expect(directive.columnHidden).toBe(false); + expect(nativeElement.columnHidden).toBe(false); + }); + + it('can be configured with attribute binding for sortDirection', () => { + expect(directive.sortDirection).toBe(TableColumnSortDirection.ascending); + expect(nativeElement.sortDirection).toBe(TableColumnSortDirection.ascending); + + fixture.componentInstance.sortDirection = TableColumnSortDirection.descending; + fixture.detectChanges(); + + expect(directive.sortDirection).toBe(TableColumnSortDirection.descending); + expect(nativeElement.sortDirection).toBe(TableColumnSortDirection.descending); + }); + + it('can be configured with attribute binding for sortIndex', () => { + expect(directive.sortIndex).toBe(0); + expect(nativeElement.sortIndex).toBe(0); + + fixture.componentInstance.sortIndex = 1; + fixture.detectChanges(); + + expect(directive.sortIndex).toBe(1); + expect(nativeElement.sortIndex).toBe(1); + }); + + it('can be configured with attribute binding for sortIndex updated to null', () => { + expect(directive.sortIndex).toBe(0); + expect(nativeElement.sortIndex).toBe(0); + + fixture.componentInstance.sortIndex = null; + fixture.detectChanges(); + + expect(directive.sortIndex).toBe(null); + expect(nativeElement.sortIndex).toBe(null); + }); + + it('can be configured with attribute binding for fractionalWidth', () => { + expect(directive.fractionalWidth).toBe(2); + expect(nativeElement.fractionalWidth).toBe(2); + + fixture.componentInstance.fractionalWidth = 1; + fixture.detectChanges(); + + expect(directive.fractionalWidth).toBe(1); + expect(nativeElement.fractionalWidth).toBe(1); + }); + + it('can be configured with attribute binding for fractionalWidth set to null', () => { + expect(directive.fractionalWidth).toBe(2); + expect(nativeElement.fractionalWidth).toBe(2); + + fixture.componentInstance.fractionalWidth = null; + fixture.detectChanges(); + + expect(directive.fractionalWidth).toBe(null); + expect(nativeElement.fractionalWidth).toBe(null); + }); + + it('can be configured with attribute binding for minPixelWidth', () => { + expect(directive.minPixelWidth).toBe(40); + expect(nativeElement.minPixelWidth).toBe(40); + + fixture.componentInstance.minPixelWidth = 50; + fixture.detectChanges(); + + expect(directive.minPixelWidth).toBe(50); + expect(nativeElement.minPixelWidth).toBe(50); + }); + + it('can be configured with attribute binding for minPixelWidth set to null', () => { + expect(directive.minPixelWidth).toBe(40); + expect(nativeElement.minPixelWidth).toBe(40); + + fixture.componentInstance.minPixelWidth = null; + fixture.detectChanges(); + + expect(directive.minPixelWidth).toBe(null); + expect(nativeElement.minPixelWidth).toBe(null); + }); + + it('can be configured with attribute binding for groupIndex', () => { + expect(directive.groupIndex).toBe(0); + expect(nativeElement.groupIndex).toBe(0); + + fixture.componentInstance.groupIndex = 1; + fixture.detectChanges(); + + expect(directive.groupIndex).toBe(1); + expect(nativeElement.groupIndex).toBe(1); + }); + + it('can be configured with attribute binding for groupIndex updated to null', () => { + expect(directive.groupIndex).toBe(0); + expect(nativeElement.groupIndex).toBe(0); + + fixture.componentInstance.groupIndex = null; + fixture.detectChanges(); + + expect(directive.groupIndex).toBe(null); + expect(nativeElement.groupIndex).toBe(null); + }); + + it('can be configured with attribute binding for groupingDisabled', () => { + expect(directive.groupingDisabled).toBe(false); + expect(nativeElement.groupingDisabled).toBe(false); + + fixture.componentInstance.groupingDisabled = true; + fixture.detectChanges(); + + expect(directive.groupingDisabled).toBe(true); + expect(nativeElement.groupingDisabled).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/change/@ni-nimble-angular-5a38f8f9-f799-46ba-8981-eb3e4c3ff9f8.json b/change/@ni-nimble-angular-5a38f8f9-f799-46ba-8981-eb3e4c3ff9f8.json new file mode 100644 index 0000000000..2a2ed43e38 --- /dev/null +++ b/change/@ni-nimble-angular-5a38f8f9-f799-46ba-8981-eb3e4c3ff9f8.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Angular support for icon table column", + "packageName": "@ni/nimble-angular", + "email": "7282195+m-akinc@users.noreply.github.com", + "dependentChangeType": "patch" +}