Skip to content

Commit

Permalink
Angular support for icon table column (#1448)
Browse files Browse the repository at this point in the history
## 🀨 Rationale

Part of #1013

## πŸ‘©β€πŸ’» Implementation

- Created directive, module, and tests for `nimble-table-column-icon`,
`nimble-mapping-icon`, and `nimble-mapping-spinner`.
- Added icon column to example app.

## πŸ§ͺ Testing

Wrote standard tests for Angular directives.

## βœ… Checklist

- [x] I have updated the project documentation to reflect my changes or
determined no changes are needed.
  • Loading branch information
m-akinc authored Aug 24, 2023
1 parent 12beb26 commit d3f5a24
Show file tree
Hide file tree
Showing 19 changed files with 1,275 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -81,6 +84,9 @@ import { HeaderComponent } from './header/header.component';
NimbleMappingTextModule,
NimbleBannerModule,
NimbleRichTextViewerModule,
NimbleTableColumnIconModule,
NimbleMappingIconModule,
NimbleMappingSpinnerModule,
RouterModule.forRoot(
[
{ path: '', redirectTo: '/customapp', pathMatch: 'full' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,22 @@
<nimble-mapping-text key="101" text="Status message 2"></nimble-mapping-text>
Status
</nimble-table-column-enum-text>
<nimble-table-column-icon
field-name="result"
key-type="string"
>
<nimble-mapping-icon
key="success"
text="Success"
icon="nimble-icon-check"
severity="success">
</nimble-mapping-icon>
<nimble-mapping-spinner
key="unknown"
text="Unknown">
</nimble-mapping-spinner>
Result
</nimble-table-column-icon>
<nimble-table-column-text field-name="stringValue2" min-pixel-width="400" group-index="0">String 2</nimble-table-column-text>

<nimble-menu slot="action-menu">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface SimpleTableRecord extends TableRecord {
linkLabel?: string;
date: number;
statusCode: number;
result: string;
}

@Component({
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public-api.ts"
}
}
Original file line number Diff line number Diff line change
@@ -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<MappingIcon>) {}
}
Original file line number Diff line number Diff line change
@@ -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 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './nimble-mapping-icon.directive';
export * from './nimble-mapping-icon.module';
Original file line number Diff line number Diff line change
@@ -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: `
<nimble-table>
<nimble-table-column-icon key-type="boolean">
<nimble-mapping-icon
#mapping
key="false"
text="nope"
icon="nimble-icon-xmark"
severity="error"
>
</nimble-mapping-icon>
</nimble-table-column-icon>
</nimble-table>
`
})
class TestHostComponent {
@ViewChild('mapping', { read: NimbleMappingIconDirective }) public directive: NimbleMappingIconDirective;
@ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef<MappingIcon>;
}

let fixture: ComponentFixture<TestHostComponent>;
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: `
<nimble-table>
<nimble-table-column-icon key-type="boolean">
<nimble-mapping-icon
#mapping
[key]="key"
[text]="text"
[icon]="icon"
[severity]="severity"
>
</nimble-mapping-icon>
</nimble-table-column-icon>
</nimble-table>
`
})
class TestHostComponent {
@ViewChild('mapping', { read: NimbleMappingIconDirective }) public directive: NimbleMappingIconDirective;
@ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef<MappingIcon>;
public key = false;
public text = 'nope';
public icon = 'nimble-icon-xmark';
public severity: IconSeverity = IconSeverity.error;
}

let fixture: ComponentFixture<TestHostComponent>;
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: `
<nimble-table>
<nimble-table-column-icon key-type="boolean">
<nimble-mapping-icon
#mapping
[attr.key]="key"
[attr.text]="text"
[attr.icon]="icon"
[attr.severity]="severity"
>
</nimble-mapping-icon>
</nimble-table-column-icon>
</nimble-table>
`
})
class TestHostComponent {
@ViewChild('mapping', { read: NimbleMappingIconDirective }) public directive: NimbleMappingIconDirective;
@ViewChild('mapping', { read: ElementRef }) public elementRef: ElementRef<MappingIcon>;
public key = false;
public text = 'nope';
public icon = 'nimble-icon-xmark';
public severity: IconSeverity = IconSeverity.error;
}

let fixture: ComponentFixture<TestHostComponent>;
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');
});
});
});
Loading

0 comments on commit d3f5a24

Please sign in to comment.