From 7f37e8ddc31153e03599f8e7eec101b32c095473 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Thu, 2 Feb 2023 17:04:10 -0600 Subject: [PATCH 01/23] partial --- .../src/table-column/base/index.ts | 5 +- .../text/tests/table-column-text.spec.ts | 2 +- packages/nimble-components/src/table/index.ts | 44 +- .../src/table/models/table-validator.spec.ts | 453 +++++++++--------- .../src/table/models/table-validator.ts | 36 +- .../nimble-components/src/table/template.ts | 25 +- .../src/table/tests/table.stories.ts | 8 +- packages/nimble-components/src/table/types.ts | 2 + 8 files changed, 306 insertions(+), 269 deletions(-) diff --git a/packages/nimble-components/src/table-column/base/index.ts b/packages/nimble-components/src/table-column/base/index.ts index 414961d441..6696c5d699 100644 --- a/packages/nimble-components/src/table-column/base/index.ts +++ b/packages/nimble-components/src/table-column/base/index.ts @@ -1,4 +1,4 @@ -import type { ElementStyles, ViewTemplate } from '@microsoft/fast-element'; +import { attr, ElementStyles, ViewTemplate } from '@microsoft/fast-element'; import { FoundationElement } from '@microsoft/fast-foundation'; import type { TableCellRecord, @@ -13,6 +13,9 @@ export abstract class TableColumn< TCellRecord extends TableCellRecord = TableCellRecord, TColumnConfig = unknown > extends FoundationElement { + @attr({ attribute: 'column-id' }) + public columnId?: string | null; + /** * The template to use to render the cell content for the column */ diff --git a/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts b/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts index 25e32c6154..56480fc4c6 100644 --- a/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts +++ b/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts @@ -62,7 +62,7 @@ describe('TableColumnText', () => { }); } - it('changing fieldName updates display', async () => { + fit('changing fieldName updates display', async () => { element.setData([{ field: 'foo', anotherField: 'bar' }]); await connect(); await waitForUpdatesAsync(); diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index ee06b7ce6a..2e301cb9f5 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -9,7 +9,7 @@ import { getCoreRowModel as tanStackGetCoreRowModel, TableOptionsResolved as TanStackTableOptionsResolved } from '@tanstack/table-core'; -import type { TableColumn } from '../table-column/base'; +import { TableColumn } from '../table-column/base'; import { TableValidator } from './models/table-validator'; import { styles } from './styles'; import { template } from './template'; @@ -38,7 +38,7 @@ export class Table< public tableData: TableRowState<TData>[] = []; @observable - public readonly columns: TableColumn[] = []; + public columns: TableColumn[] = []; /** * @internal @@ -55,6 +55,9 @@ export class Table< */ public readonly viewport!: HTMLElement; + @observable + public childItems: Element[] = []; + /** * @internal */ @@ -84,18 +87,12 @@ export class Table< this.setTableData(newData); } - public idFieldNameChanged( - _prev: string | undefined, - _next: string | undefined - ): void { - // Force TanStack to detect a data update because a row's ID is only - // generated when creating a new row model. - this.setTableData(this.table.options.data); - } - public override connectedCallback(): void { super.connectedCallback(); this.virtualizer.connectedCallback(); + + this.tableValidator.validateColumnIds(this.columns.map(x => x.columnId)); + this.canRenderRows = this.checkValidity(); } public override disconnectedCallback(): void { @@ -106,6 +103,31 @@ export class Table< return this.tableValidator.isValid(); } + private idFieldNameChanged( + _prev: string | undefined, + _next: string | undefined + ): void { + // Force TanStack to detect a data update because a row's ID is only + // generated when creating a new row model. + this.setTableData(this.table.options.data); + } + + private columnsChanged( + _prev: TableColumn[] | undefined, + _next: TableColumn[] + ): void { + if (!this.$fastController.isConnected) { + return; + } + + this.tableValidator.validateColumnIds(this.columns.map(x => x.columnId)); + this.canRenderRows = this.checkValidity(); + } + + private childItemsChanged(_prev: undefined | Element[], next: Element[]): void { + this.columns = next.filter(x => x instanceof TableColumn).map(x => x as TableColumn); + } + private setTableData(newData: readonly TData[]): void { const data = newData.map(record => { return { ...record }; diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index b7f41cf68d..74b1b285ef 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -1,250 +1,229 @@ +import type { TableRecord, TableValidity } from '../types'; import { TableValidator } from './table-validator'; -describe('TableValidator', () => { - it('setting valid field for ID is valid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting `undefined` field for ID is valid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, undefined); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting `null` field for ID is valid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, null); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting data with duplicate IDs is invalid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-1', numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeTrue(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting data with invalid ID value type is invalid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'numberField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeTrue(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting data with empty ID value is valid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: '', numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting data with undefined ID value is invalid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: undefined, numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeTrue(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting data with null ID value is invalid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: undefined, numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeTrue(); - expect(validity.missingRecordId).toBeFalse(); - }); - - it('setting data with missing IDs is invalid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'missingField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeTrue(); - }); - - it('multiple errors are reported during validation', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 }, - { stringField: 'value-1', numberField: 12 }, - { numberField: 12 }, - { stringField: true, numberField: 12 } - ]; - const validator = new TableValidator(); - - const isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeTrue(); - expect(validity.invalidRecordId).toBeTrue(); - expect(validity.missingRecordId).toBeTrue(); - }); - - it('setting ID field name to undefined makes configuration valid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); - - let isValid = validator.validateRecordIds(data, 'missingField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - - isValid = validator.validateRecordIds(data, undefined); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); - }); - - it('setting a valid ID field name makes an invalid configuration valid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); +fdescribe('TableValidator', () => { + let validator: TableValidator<TableRecord>; - let isValid = validator.validateRecordIds(data, 'missingField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - - isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); - }); - - it('setting invalid ID field name makes a valid configuration invalid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - const validator = new TableValidator(); - - let isValid = validator.validateRecordIds(data, 'stringField'); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); - - isValid = validator.validateRecordIds(data, 'missingField'); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); + beforeEach(() => { + validator = new TableValidator(); }); - it('ID field name can be an empty string', () => { - const data = [ - // eslint-disable-next-line @typescript-eslint/naming-convention - { stringField: 'value-1', numberField: 10, '': 'empty-1' }, - // eslint-disable-next-line @typescript-eslint/naming-convention - { stringField: 'value-2', numberField: 11, '': 'empty-2' } - ]; - const validator = new TableValidator(); + function validateValidity(invalidKeys: (keyof TableValidity)[]): void { + expect(validator.isValid()).toEqual(invalidKeys.length === 0); - const isValid = validator.validateRecordIds(data, ''); - expect(isValid).toBeTrue(); - expect(validator.isValid()).toBeTrue(); const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeFalse(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeFalse(); + expect(validity.duplicateRecordId).toBe(invalidKeys.some(x => x === 'duplicateRecordId')); + expect(validity.missingRecordId).toBe(invalidKeys.some(x => x === 'missingRecordId')); + expect(validity.invalidRecordId).toBe(invalidKeys.some(x => x === 'invalidRecordId')); + expect(validity.duplicateColumnId).toBe(invalidKeys.some(x => x === 'duplicateColumnId')); + expect(validity.missingColumnId).toBe(invalidKeys.some(x => x === 'missingColumnId')); + } + + describe('record ID validation', () => { + it('setting valid field for ID is valid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeTrue(); + validateValidity([]); + }); + + it('setting `undefined` field for ID is valid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, undefined); + expect(isValid).toBeTrue(); + validateValidity([]); + }); + + it('setting `null` field for ID is valid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, null); + expect(isValid).toBeTrue(); + validateValidity([]); + }); + + it('setting data with duplicate IDs is invalid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-1', numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeFalse(); + validateValidity(['duplicateRecordId']); + }); + + it('setting data with invalid ID value type is invalid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, 'numberField'); + expect(isValid).toBeFalse(); + validateValidity(['invalidRecordId']); + }); + + it('setting data with empty ID value is valid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: '', numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeTrue(); + validateValidity([]); + }); + + it('setting data with undefined ID value is invalid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: undefined, numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeFalse(); + validateValidity(['invalidRecordId']); + }); + + it('setting data with null ID value is invalid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: undefined, numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeFalse(); + validateValidity(['invalidRecordId']); + }); + + it('setting data with missing IDs is invalid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + const isValid = validator.validateRecordIds(data, 'missingField'); + expect(isValid).toBeFalse(); + validateValidity(['missingRecordId']); + }); + + it('multiple errors are reported during validation', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 }, + { stringField: 'value-1', numberField: 12 }, + { numberField: 12 }, + { stringField: true, numberField: 12 } + ]; + + const isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeFalse(); + validateValidity(['missingRecordId', 'duplicateRecordId', 'invalidRecordId']); + }); + + it('setting ID field name to undefined makes configuration valid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + let isValid = validator.validateRecordIds(data, 'missingField'); + expect(isValid).toBeFalse(); + expect(validator.isValid()).toBeFalse(); + + isValid = validator.validateRecordIds(data, undefined); + expect(isValid).toBeTrue(); + expect(validator.isValid()).toBeTrue(); + }); + + it('setting a valid ID field name makes an invalid configuration valid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + let isValid = validator.validateRecordIds(data, 'missingField'); + expect(isValid).toBeFalse(); + expect(validator.isValid()).toBeFalse(); + + isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeTrue(); + expect(validator.isValid()).toBeTrue(); + }); + + it('setting invalid ID field name makes a valid configuration invalid', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 } + ]; + + let isValid = validator.validateRecordIds(data, 'stringField'); + expect(isValid).toBeTrue(); + expect(validator.isValid()).toBeTrue(); + + isValid = validator.validateRecordIds(data, 'missingField'); + expect(isValid).toBeFalse(); + expect(validator.isValid()).toBeFalse(); + }); + + it('ID field name can be an empty string', () => { + const data = [ + // eslint-disable-next-line @typescript-eslint/naming-convention + { stringField: 'value-1', numberField: 10, '': 'empty-1' }, + // eslint-disable-next-line @typescript-eslint/naming-convention + { stringField: 'value-2', numberField: 11, '': 'empty-2' } + ]; + + const isValid = validator.validateRecordIds(data, ''); + expect(isValid).toBeTrue(); + validateValidity([]); + }); + + it('validation occurs when ID field name is an empty string', () => { + const data = [ + // eslint-disable-next-line @typescript-eslint/naming-convention + { stringField: 'value-1', numberField: 10, '': 'empty-1' }, + // eslint-disable-next-line @typescript-eslint/naming-convention + { stringField: 'value-2', numberField: 11, '': 'empty-1' } + ]; + + const isValid = validator.validateRecordIds(data, ''); + expect(isValid).toBeFalse(); + validateValidity(['duplicateRecordId']); + }); }); - it('validation occurs when ID field name is an empty string', () => { - const data = [ - // eslint-disable-next-line @typescript-eslint/naming-convention - { stringField: 'value-1', numberField: 10, '': 'empty-1' }, - // eslint-disable-next-line @typescript-eslint/naming-convention - { stringField: 'value-2', numberField: 11, '': 'empty-1' } + describe('column ID validation', () => { + const columnConfigurations: { columnIds: (string | undefined | null)[], invalidKeys: (keyof TableValidity)[], testDescription: string }[] = [ + { columnIds: [null, null, undefined, undefined], invalidKeys: [], testDescription: 'does not require column IDs' }, + { columnIds: [null, 'my-id', undefined, undefined], invalidKeys: ['missingColumnId'], testDescription: 'requires column IDs for all columns if a column ID is defined for at least one' }, + { columnIds: ['my-id-1', 'my-id-2', 'my-id-3'], invalidKeys: [], testDescription: 'unique defined IDs are valid' }, + { columnIds: ['my-id-1', 'my-id-2', 'my-id-2'], invalidKeys: ['duplicateColumnId'], testDescription: 'duplicate column IDs is invalid' }, + { columnIds: ['my-id-1', 'my-id-2', 'my-id-2', undefined], invalidKeys: ['missingColumnId', 'duplicateColumnId'], testDescription: 'reports multiple column ID validation errors' }, + { columnIds: ['my-id-1', 'my-id-2', ''], invalidKeys: [], testDescription: 'empty string is a valid column ID' }, + { columnIds: ['my-id-1', '', ''], invalidKeys: ['duplicateColumnId'], testDescription: 'detects duplicate empty string column IDs' }, + { columnIds: [null, undefined, ''], invalidKeys: ['missingColumnId'], testDescription: 'requires column IDs for all columns if one is set to empty string' }, ]; - const validator = new TableValidator(); - const isValid = validator.validateRecordIds(data, ''); - expect(isValid).toBeFalse(); - expect(validator.isValid()).toBeFalse(); - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBeTrue(); - expect(validity.invalidRecordId).toBeFalse(); - expect(validity.missingRecordId).toBeFalse(); + for (const columnConfiguration of columnConfigurations) { + // eslint-disable-next-line @typescript-eslint/no-loop-func + it(columnConfiguration.testDescription, () => { + const isValid = validator.validateColumnIds(columnConfiguration.columnIds); + expect(isValid).toBe(columnConfiguration.invalidKeys.length === 0); + validateValidity(columnConfiguration.invalidKeys); + }); + } }); }); diff --git a/packages/nimble-components/src/table/models/table-validator.ts b/packages/nimble-components/src/table/models/table-validator.ts index f7bfebdffa..57ce5e3ab4 100644 --- a/packages/nimble-components/src/table/models/table-validator.ts +++ b/packages/nimble-components/src/table/models/table-validator.ts @@ -1,3 +1,4 @@ +import type { TableColumn } from '../../table-column/base'; import type { TableRecord, TableValidity } from '../types'; /** @@ -8,12 +9,16 @@ export class TableValidator<TData extends TableRecord> { private duplicateRecordId = false; private missingRecordId = false; private invalidRecordId = false; + private duplicateColumnId = false; + private missingColumnId = false; public getValidity(): TableValidity { return { duplicateRecordId: this.duplicateRecordId, missingRecordId: this.missingRecordId, - invalidRecordId: this.invalidRecordId + invalidRecordId: this.invalidRecordId, + duplicateColumnId: this.duplicateColumnId, + missingColumnId: this.missingColumnId }; } @@ -59,4 +64,33 @@ export class TableValidator<TData extends TableRecord> { && !this.duplicateRecordId ); } + + public validateColumnIds(columnIds: (string | null | undefined)[]): boolean { + this.missingColumnId = false; + this.duplicateColumnId = false; + + const anyColumnsHaveIds = columnIds.some(columnId => columnId !== undefined && columnId !== null); + + if (!anyColumnsHaveIds) { + return true; + } + + const idSet = new Set<string>(); + for (const columnId of columnIds) { + if (typeof columnId !== 'string') { + this.missingColumnId = true; + continue; + } + + if (idSet.has(columnId)) { + this.duplicateColumnId = true; + } + idSet.add(columnId); + } + + return ( + !this.missingColumnId + && !this.duplicateColumnId + ); + } } diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index 9cd81a35bf..251a8d5319 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -1,4 +1,5 @@ import { + children, ElementsFilter, html, ref, @@ -11,22 +12,19 @@ import type { VirtualItem } from '@tanstack/virtual-core'; import type { Table } from '.'; import { TableHeader } from './components/header'; import { TableRow } from './components/row'; -import { TableColumn } from '../table-column/base'; - -const isTableColumn = (): ElementsFilter => { - const filter: ElementsFilter = ( - value: Node, - _: number, - __: Node[] - ): boolean => { - return value instanceof TableColumn; - }; - return filter; -}; +import type { TableColumn } from '../table-column/base'; // prettier-ignore export const template = html<Table>` - <template role="table"> + <template + role="table" + ${children({ + property: 'childItems', + attributeFilter: ['column-id', 'field-name'], + subtree: true, + selector: '*' + })} + > <div class="table-container"> <div role="rowgroup" class="header-container" style="margin-right: ${x => x.virtualizer.headerContainerMarginRight}px;"> <div class="header-row" role="row"> @@ -55,6 +53,5 @@ export const template = html<Table>` </div> </div> </div> - <slot ${slotted({ property: 'columns', filter: isTableColumn() })}></slot> </template> `; diff --git a/packages/nimble-components/src/table/tests/table.stories.ts b/packages/nimble-components/src/table/tests/table.stories.ts index ce816a45d0..cd23dd5fdc 100644 --- a/packages/nimble-components/src/table/tests/table.stories.ts +++ b/packages/nimble-components/src/table/tests/table.stories.ts @@ -125,10 +125,10 @@ const metadata: Meta<TableArgs> = { id-field-name="${x => dataSetIdFieldNames[x.data]}" data-unused="${x => x.updateData(x)}" > - <nimble-table-column-text field-name="firstName" placeholder="no value">First Name</nimble-table-column-text> - <nimble-table-column-text field-name="lastName" placeholder="no value">Last Name</nimble-table-column-text> - <nimble-table-column-text field-name="favoriteColor" placeholder="no value">Favorite Color</nimble-table-column-text> - <nimble-table-column-text field-name="quote" placeholder="no value">Quote</nimble-table-column-text> + <nimble-table-column-text field-name="firstName" placeholder="no value" column-id="foo">First Name</nimble-table-column-text> + <nimble-table-column-text field-name="lastName" placeholder="no value" column-id="food">Last Name</nimble-table-column-text> + <nimble-table-column-text field-name="favoriteColor" placeholder="no value" column-id="bar">Favorite Color</nimble-table-column-text> + <nimble-table-column-text field-name="quote" placeholder="no value" column-id="baz">Quote</nimble-table-column-text> </nimble-table> <style class="code-hide"> #usage-warning { diff --git a/packages/nimble-components/src/table/types.ts b/packages/nimble-components/src/table/types.ts index d55547d5bb..115f028b6d 100644 --- a/packages/nimble-components/src/table/types.ts +++ b/packages/nimble-components/src/table/types.ts @@ -46,6 +46,8 @@ export interface TableValidity { readonly duplicateRecordId: boolean; readonly missingRecordId: boolean; readonly invalidRecordId: boolean; + readonly duplicateColumnId: boolean; + readonly missingColumnId: boolean; } export interface TableRowState<TData extends TableRecord = TableRecord> { From 1c9a69b6644594aec4f652c74f01fabda3bdf1e0 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 10:11:10 -0600 Subject: [PATCH 02/23] more tests --- .../text/tests/table-column-text.spec.ts | 2 +- .../src/table/models/table-validator.spec.ts | 32 +++++++- .../src/table/tests/table.spec.ts | 81 +++++++++++++++++-- 3 files changed, 108 insertions(+), 7 deletions(-) diff --git a/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts b/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts index 56480fc4c6..25e32c6154 100644 --- a/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts +++ b/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts @@ -62,7 +62,7 @@ describe('TableColumnText', () => { }); } - fit('changing fieldName updates display', async () => { + it('changing fieldName updates display', async () => { element.setData([{ field: 'foo', anotherField: 'bar' }]); await connect(); await waitForUpdatesAsync(); diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index 74b1b285ef..965e348288 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -1,7 +1,7 @@ import type { TableRecord, TableValidity } from '../types'; import { TableValidator } from './table-validator'; -fdescribe('TableValidator', () => { +describe('TableValidator', () => { let validator: TableValidator<TableRecord>; beforeEach(() => { @@ -226,4 +226,34 @@ fdescribe('TableValidator', () => { }); } }); + + describe('validation checks do not reset unrelated state', () => { + it('invalid record IDs stay invalid when validating column IDs', () => { + const data = [ + { stringField: 'value-1', numberField: 10 }, + { stringField: 'value-2', numberField: 11 }, + { stringField: 'value-1', numberField: 12 }, + { numberField: 12 }, + { stringField: true, numberField: 12 } + ]; + + const recordIdsAreValid = validator.validateRecordIds(data, 'stringField'); + expect(recordIdsAreValid).toBeFalse(); + validateValidity(['missingRecordId', 'duplicateRecordId', 'invalidRecordId']); + + const columnIdsAreValid = validator.validateColumnIds(['id-1', 'id-2', 'id-3']); + expect(columnIdsAreValid).toBeTrue(); + validateValidity(['missingRecordId', 'duplicateRecordId', 'invalidRecordId']); + }); + + it('invalid column IDs stay invalid when validating record IDs', () => { + const columnIdsAreValid = validator.validateColumnIds(['id-1', 'id-1', undefined]); + expect(columnIdsAreValid).toBeFalse(); + validateValidity(['missingColumnId', 'duplicateColumnId']); + + const recordIdsAreValid = validator.validateRecordIds([], undefined); + expect(recordIdsAreValid).toBeTrue(); + validateValidity(['missingColumnId', 'duplicateColumnId']); + }); + }); }); diff --git a/packages/nimble-components/src/table/tests/table.spec.ts b/packages/nimble-components/src/table/tests/table.spec.ts index cdc892a2ee..41a8e4edc8 100644 --- a/packages/nimble-components/src/table/tests/table.spec.ts +++ b/packages/nimble-components/src/table/tests/table.spec.ts @@ -1,6 +1,7 @@ import { html } from '@microsoft/fast-element'; import { DesignSystem } from '@microsoft/fast-foundation'; import { Table } from '..'; +import type { TableColumn } from '../../table-column/base'; import { TableColumnText } from '../../table-column/text'; import { waitForUpdatesAsync } from '../../testing/async-helpers'; import { controlHeight } from '../../theme-provider/design-tokens'; @@ -40,14 +41,10 @@ const largeTableData = Array.from(Array(500), (_, i) => { }; }); -const tableColumnText = DesignSystem.tagFor(TableColumnText); - // prettier-ignore async function setup(): Promise<Fixture<Table<SimpleTableRecord>>> { return fixture<Table<SimpleTableRecord>>( - html`<nimble-table> - <${tableColumnText} field-name="stringData">stringData</${tableColumnText}> - </nimble-table>` + html`<nimble-table></nimble-table>` ); } @@ -56,6 +53,8 @@ describe('Table', () => { let connect: () => Promise<void>; let disconnect: () => Promise<void>; let pageObject: TablePageObject<SimpleTableRecord>; + let column1: TableColumn; + let column2: TableColumn; // The assumption being made here is that the values in the data are equal to their // rendered representation (no formatting). @@ -108,6 +107,17 @@ describe('Table', () => { beforeEach(async () => { ({ element, connect, disconnect } = await setup()); pageObject = new TablePageObject<SimpleTableRecord>(element); + + const tableColumnTextTag = DesignSystem.tagFor(TableColumnText); + column1 = document.createElement(tableColumnTextTag) as TableColumn; + column1.textContent = 'stringData'; + (column1 as TableColumnText).fieldName = 'stringData'; + column2 = document.createElement(tableColumnTextTag) as TableColumn; + column2.textContent = 'moreStringData'; + (column2 as TableColumnText).fieldName = 'stringData'; + + element.appendChild(column1); + element.appendChild(column2); }); afterEach(async () => { @@ -414,6 +424,67 @@ describe('Table', () => { }); }); + describe('column IDs', () => { + it('duplicate column IDs marks the table as invalid and rows are not rendered', async () => { + element.setData(simpleTableData); + column1.columnId = 'my-column-id'; + column2.columnId = 'my-column-id'; + await connect(); + await waitForUpdatesAsync(); + + expect(pageObject.getRenderedRowCount()).toBe(0); + expect(element.checkValidity()).toBeFalse(); + expect(element.validity.duplicateColumnId).toBeTrue(); + expect(element.validity.missingColumnId).toBeFalse(); + }); + + it('missing column IDs marks the table as invalid and rows are not rendered', async () => { + element.setData(simpleTableData); + column1.columnId = 'my-column-id'; + column2.columnId = undefined; + await connect(); + await waitForUpdatesAsync(); + + expect(pageObject.getRenderedRowCount()).toBe(0); + expect(element.checkValidity()).toBeFalse(); + expect(element.validity.duplicateColumnId).toBeFalse(); + expect(element.validity.missingColumnId).toBeTrue(); + }); + + it('table validity updates if column IDs become valid', async () => { + element.setData(simpleTableData); + column1.columnId = 'my-column-id'; + column2.columnId = undefined; + await connect(); + await waitForUpdatesAsync(); + + expect(pageObject.getRenderedRowCount()).toBe(0); + expect(element.checkValidity()).toBeFalse(); + + column2.setAttribute('column-id', 'my-other-column-id'); + await waitForUpdatesAsync(); + + verifyRenderedData(simpleTableData); + expect(element.checkValidity()).toBeTrue(); + }); + + it('table validity updates if column IDs become invalid', async () => { + element.setData(simpleTableData); + await connect(); + await waitForUpdatesAsync(); + + verifyRenderedData(simpleTableData); + expect(element.checkValidity()).toBeTrue(); + + column1.setAttribute('column-id', 'my-column-id'); + column2.setAttribute('column-id', 'my-column-id'); + await waitForUpdatesAsync(); + + expect(pageObject.getRenderedRowCount()).toBe(0); + expect(element.checkValidity()).toBeFalse(); + }); + }); + describe('uses virtualization', () => { it('to render fewer rows (based on viewport size)', async () => { await connect(); From 6de11a49af1642d67abdead5272f3bc9cd47081d Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 10:13:45 -0600 Subject: [PATCH 03/23] format --- .../src/table/models/table-validator.ts | 1 - packages/nimble-components/src/table/template.ts | 12 +++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/nimble-components/src/table/models/table-validator.ts b/packages/nimble-components/src/table/models/table-validator.ts index 57ce5e3ab4..76e1d71b9c 100644 --- a/packages/nimble-components/src/table/models/table-validator.ts +++ b/packages/nimble-components/src/table/models/table-validator.ts @@ -1,4 +1,3 @@ -import type { TableColumn } from '../../table-column/base'; import type { TableRecord, TableValidity } from '../types'; /** diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index 251a8d5319..22ff9d9542 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -1,10 +1,8 @@ import { children, - ElementsFilter, html, ref, repeat, - slotted, when } from '@microsoft/fast-element'; import { DesignSystem } from '@microsoft/fast-foundation'; @@ -19,11 +17,11 @@ export const template = html<Table>` <template role="table" ${children({ - property: 'childItems', - attributeFilter: ['column-id', 'field-name'], - subtree: true, - selector: '*' - })} + property: 'childItems', + attributeFilter: ['column-id'], + subtree: true, + selector: '*' + })} > <div class="table-container"> <div role="rowgroup" class="header-container" style="margin-right: ${x => x.virtualizer.headerContainerMarginRight}px;"> From 037ac7e617ca00b98cc45865ee2486fe1c0affe3 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 10:30:09 -0600 Subject: [PATCH 04/23] Update TableValidity interface in Blazor --- packages/nimble-blazor/NimbleBlazor/TableValidity.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/nimble-blazor/NimbleBlazor/TableValidity.cs b/packages/nimble-blazor/NimbleBlazor/TableValidity.cs index de35c8e6d6..67ab1f58da 100644 --- a/packages/nimble-blazor/NimbleBlazor/TableValidity.cs +++ b/packages/nimble-blazor/NimbleBlazor/TableValidity.cs @@ -9,6 +9,10 @@ public interface ITableValidity public bool MissingRecordId { get; } public bool InvalidRecordId { get; } + + public bool DuplicateColumnId { get; } + + public bool MissingColumnId { get; } } internal class TableValidity : ITableValidity @@ -21,4 +25,10 @@ internal class TableValidity : ITableValidity [JsonPropertyName("invalidRecordId")] public bool InvalidRecordId { get; set; } + + [JsonPropertyName("duplicateColumnId")] + public bool DuplicateColumnId { get; set; } + + [JsonPropertyName("missingColumnId")] + public bool MissingColumnId { get; set; } } From 6082e59cfc2115ea14aa2d5e1c2c7a03c564aae0 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 10:45:22 -0600 Subject: [PATCH 05/23] column id support in blazor --- .../NimbleBlazor/Components/NimbleTableColumnText.razor | 2 +- .../NimbleBlazor/Components/NimbleTableColumnText.razor.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor index 26a2dc5a54..2ed123db5f 100644 --- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor +++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor @@ -1,6 +1,6 @@ @namespace NimbleBlazor -<nimble-table-column-text field-name="@FieldName" placeholder="@Placeholder"> +<nimble-table-column-text column-id="@ColumnId" field-name="@FieldName" placeholder="@Placeholder"> @ChildContent </nimble-table-column-text> diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs index 16947dd289..a7aef0a30c 100644 --- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs +++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs @@ -5,6 +5,12 @@ namespace NimbleBlazor; public partial class NimbleTableColumnText : ComponentBase { + /// <summary> + /// The ID of the column of a <see cref="NimbleTable{TData}"/> + /// </summary> + [Parameter] + public string? ColumnId { get; set; } + /// <summary> /// Gets or sets the field in the element representing a row of data in a <see cref="NimbleTable{TData}"/>to display /// </summary> From 03e0a8fe3224e0a705b4401fd53aef8151a54191 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 11:02:57 -0600 Subject: [PATCH 06/23] Angular --- .../nimble-table-column-text.directive.ts | 10 ++++ ...nimble-table-column-text.directive.spec.ts | 51 ++++++++++++++++--- .../tests/nimble-table.directive.spec.ts | 4 +- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts b/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts index 4a14931539..66416fa0bf 100644 --- a/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts +++ b/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts @@ -10,6 +10,16 @@ export type { TableColumnText }; selector: 'nimble-table-column-text' }) export class NimbleTableColumnTextDirective { + public get columnId(): string | null | undefined { + return this.elementRef.nativeElement.columnId; + } + + // Renaming because property should have camel casing, but attribute should not + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('column-id') public set columnId(value: string | null | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'columnId', value); + } + public get fieldName(): string | undefined { return this.elementRef.nativeElement.fieldName; } diff --git a/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/tests/nimble-table-column-text.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/tests/nimble-table-column-text.directive.spec.ts index 0d723a2d36..4151951853 100644 --- a/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/tests/nimble-table-column-text.directive.spec.ts +++ b/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/tests/nimble-table-column-text.directive.spec.ts @@ -21,7 +21,7 @@ describe('NimbleTableColumnText', () => { @Component({ template: ` <nimble-table> - <nimble-table-column-text #column field-name="field1" placeholder="no value"></nimble-table-column-text> + <nimble-table-column-text #column column-id="my-column" field-name="field1" placeholder="no value"></nimble-table-column-text> </nimble-table> ` }) @@ -50,17 +50,27 @@ describe('NimbleTableColumnText', () => { expect(nativeElement.fieldName).toBe('field1'); }); - it('cwill use template string values for placeholder', () => { + it('will use template string values for placeholder', () => { expect(directive.placeholder).toBe('no value'); expect(nativeElement.placeholder).toBe('no value'); }); + + it('will use template string values for columnId', () => { + expect(directive.columnId).toBe('my-column'); + expect(nativeElement.columnId).toBe('my-column'); + }); }); describe('with property bound values', () => { @Component({ template: ` <nimble-table> - <nimble-table-column-text #column [field-name]="field" [placeholder]="placeholder"></nimble-table-column-text> + <nimble-table-column-text + #column + [column-id]="columnId" + [field-name]="field" + [placeholder]="placeholder" + ></nimble-table-column-text> </nimble-table> ` }) @@ -69,6 +79,7 @@ describe('NimbleTableColumnText', () => { @ViewChild('column', { read: ElementRef }) public elementRef: ElementRef<TableColumnText>; public field = 'field1'; public placeholder = 'no value'; + public columnId = 'my-column'; } let fixture: ComponentFixture<TestHostComponent>; @@ -107,13 +118,29 @@ describe('NimbleTableColumnText', () => { expect(directive.placeholder).toBe('foo'); expect(nativeElement.placeholder).toBe('foo'); }); + + 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'); + }); }); describe('with attribute bound values', () => { @Component({ template: ` <nimble-table> - <nimble-table-column-text #column [attr.field-name]="field" [attr.placeholder]="placeholder"></nimble-table-column-text> + <nimble-table-column-text + #column + [attr.column-id]="columnId" + [attr.field-name]="field" + [attr.placeholder]="placeholder" + ></nimble-table-column-text> </nimble-table> ` }) @@ -122,6 +149,7 @@ describe('NimbleTableColumnText', () => { @ViewChild('column', { read: ElementRef }) public elementRef: ElementRef<TableColumnText>; public field = 'field1'; public placeholder = 'no value'; + public columnId = 'my-column'; } let fixture: ComponentFixture<TestHostComponent>; @@ -139,7 +167,7 @@ describe('NimbleTableColumnText', () => { nativeElement = fixture.componentInstance.elementRef.nativeElement; }); - it('can be configured with property binding for fieldName', () => { + it('can be configured with attribute binding for fieldName', () => { expect(directive.fieldName).toBe('field1'); expect(nativeElement.fieldName).toBe('field1'); @@ -150,7 +178,7 @@ describe('NimbleTableColumnText', () => { expect(nativeElement.fieldName).toBe('field2'); }); - it('can be configured with property binding for placeholder', () => { + it('can be configured with attribute binding for placeholder', () => { expect(directive.placeholder).toBe('no value'); expect(nativeElement.placeholder).toBe('no value'); @@ -160,5 +188,16 @@ describe('NimbleTableColumnText', () => { expect(directive.placeholder).toBe('foo'); expect(nativeElement.placeholder).toBe('foo'); }); + + 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'); + }); }); }); diff --git a/angular-workspace/projects/ni/nimble-angular/src/directives/table/tests/nimble-table.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/src/directives/table/tests/nimble-table.directive.spec.ts index 16d614088f..1dc02fd61c 100644 --- a/angular-workspace/projects/ni/nimble-angular/src/directives/table/tests/nimble-table.directive.spec.ts +++ b/angular-workspace/projects/ni/nimble-angular/src/directives/table/tests/nimble-table.directive.spec.ts @@ -149,7 +149,9 @@ describe('Nimble table', () => { const expectedValidity: TableValidity = { duplicateRecordId: false, invalidRecordId: false, - missingRecordId: true + missingRecordId: true, + duplicateColumnId: false, + missingColumnId: false }; expect(directive.validity).toEqual(expectedValidity); expect(nativeElement.validity).toEqual(expectedValidity); From 7120bde6482a785f64aa946093b9a947cb0058cd Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 11:08:45 -0600 Subject: [PATCH 07/23] doc update --- packages/nimble-components/src/table/tests/table.stories.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nimble-components/src/table/tests/table.stories.ts b/packages/nimble-components/src/table/tests/table.stories.ts index cd23dd5fdc..6481d9aad8 100644 --- a/packages/nimble-components/src/table/tests/table.stories.ts +++ b/packages/nimble-components/src/table/tests/table.stories.ts @@ -94,7 +94,9 @@ The object's type is \`TableValidityState\`, and it contains the following boole - \`duplicateRecordId\`: \`true\` when multiple records were found with the same ID - \`missingRecordId\`: \`true\` when a record was found that did not have a field with the name specified by \`id-field-name\` -- \`invalidRecordId\`: \`true\` when record was found where \`id-field-name\` did not refer to a value of type \`string\` +- \`invalidRecordId\`: \`true\` when a record was found where \`id-field-name\` did not refer to a value of type \`string\` +- \`duplicateColumnId\`: \`true\` when multiple columns were defined with the same \`column-id\` +- \`invalidColumnId\`: \`true\` when a \`column-id\` was specified for some, but not all, columns `; const metadata: Meta<TableArgs> = { From 9138e19b61855ce7ac794e67f39edf93da3b3831 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 11:12:31 -0600 Subject: [PATCH 08/23] Change files --- ...imble-angular-a552c61a-2254-4ebc-a7cd-91705f4ac48d.json | 7 +++++++ ...nimble-blazor-aa544dba-a6aa-439a-a7d0-46c522cd4d7a.json | 7 +++++++ ...le-components-a5002c3f-0312-4437-b9eb-beb9b6e218db.json | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 change/@ni-nimble-angular-a552c61a-2254-4ebc-a7cd-91705f4ac48d.json create mode 100644 change/@ni-nimble-blazor-aa544dba-a6aa-439a-a7d0-46c522cd4d7a.json create mode 100644 change/@ni-nimble-components-a5002c3f-0312-4437-b9eb-beb9b6e218db.json diff --git a/change/@ni-nimble-angular-a552c61a-2254-4ebc-a7cd-91705f4ac48d.json b/change/@ni-nimble-angular-a552c61a-2254-4ebc-a7cd-91705f4ac48d.json new file mode 100644 index 0000000000..8cb30ced2e --- /dev/null +++ b/change/@ni-nimble-angular-a552c61a-2254-4ebc-a7cd-91705f4ac48d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add column IDs to table", + "packageName": "@ni/nimble-angular", + "email": "20542556+mollykreis@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@ni-nimble-blazor-aa544dba-a6aa-439a-a7d0-46c522cd4d7a.json b/change/@ni-nimble-blazor-aa544dba-a6aa-439a-a7d0-46c522cd4d7a.json new file mode 100644 index 0000000000..033d84559c --- /dev/null +++ b/change/@ni-nimble-blazor-aa544dba-a6aa-439a-a7d0-46c522cd4d7a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add column IDs to table", + "packageName": "@ni/nimble-blazor", + "email": "20542556+mollykreis@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@ni-nimble-components-a5002c3f-0312-4437-b9eb-beb9b6e218db.json b/change/@ni-nimble-components-a5002c3f-0312-4437-b9eb-beb9b6e218db.json new file mode 100644 index 0000000000..56f56e28a9 --- /dev/null +++ b/change/@ni-nimble-components-a5002c3f-0312-4437-b9eb-beb9b6e218db.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add column IDs to table", + "packageName": "@ni/nimble-components", + "email": "20542556+mollykreis@users.noreply.github.com", + "dependentChangeType": "patch" +} From 7bfd14ebac427a68d24fe9e6bc7843c895c57da8 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 3 Feb 2023 11:13:25 -0600 Subject: [PATCH 09/23] format --- packages/nimble-components/src/table/index.ts | 17 ++- .../src/table/models/table-validator.spec.ts | 126 ++++++++++++++---- .../src/table/models/table-validator.ts | 13 +- .../nimble-components/src/table/template.ts | 8 +- 4 files changed, 123 insertions(+), 41 deletions(-) diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index 2e301cb9f5..eaca0e2696 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -91,7 +91,9 @@ export class Table< super.connectedCallback(); this.virtualizer.connectedCallback(); - this.tableValidator.validateColumnIds(this.columns.map(x => x.columnId)); + this.tableValidator.validateColumnIds( + this.columns.map(x => x.columnId) + ); this.canRenderRows = this.checkValidity(); } @@ -120,12 +122,19 @@ export class Table< return; } - this.tableValidator.validateColumnIds(this.columns.map(x => x.columnId)); + this.tableValidator.validateColumnIds( + this.columns.map(x => x.columnId) + ); this.canRenderRows = this.checkValidity(); } - private childItemsChanged(_prev: undefined | Element[], next: Element[]): void { - this.columns = next.filter(x => x instanceof TableColumn).map(x => x as TableColumn); + private childItemsChanged( + _prev: undefined | Element[], + next: Element[] + ): void { + this.columns = next + .filter(x => x instanceof TableColumn) + .map(x => x as TableColumn); } private setTableData(newData: readonly TData[]): void { diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index 965e348288..3126a5b9ec 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -12,11 +12,21 @@ describe('TableValidator', () => { expect(validator.isValid()).toEqual(invalidKeys.length === 0); const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBe(invalidKeys.some(x => x === 'duplicateRecordId')); - expect(validity.missingRecordId).toBe(invalidKeys.some(x => x === 'missingRecordId')); - expect(validity.invalidRecordId).toBe(invalidKeys.some(x => x === 'invalidRecordId')); - expect(validity.duplicateColumnId).toBe(invalidKeys.some(x => x === 'duplicateColumnId')); - expect(validity.missingColumnId).toBe(invalidKeys.some(x => x === 'missingColumnId')); + expect(validity.duplicateRecordId).toBe( + invalidKeys.some(x => x === 'duplicateRecordId') + ); + expect(validity.missingRecordId).toBe( + invalidKeys.some(x => x === 'missingRecordId') + ); + expect(validity.invalidRecordId).toBe( + invalidKeys.some(x => x === 'invalidRecordId') + ); + expect(validity.duplicateColumnId).toBe( + invalidKeys.some(x => x === 'duplicateColumnId') + ); + expect(validity.missingColumnId).toBe( + invalidKeys.some(x => x === 'missingColumnId') + ); } describe('record ID validation', () => { @@ -130,7 +140,11 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); - validateValidity(['missingRecordId', 'duplicateRecordId', 'invalidRecordId']); + validateValidity([ + 'missingRecordId', + 'duplicateRecordId', + 'invalidRecordId' + ]); }); it('setting ID field name to undefined makes configuration valid', () => { @@ -206,22 +220,64 @@ describe('TableValidator', () => { }); describe('column ID validation', () => { - const columnConfigurations: { columnIds: (string | undefined | null)[], invalidKeys: (keyof TableValidity)[], testDescription: string }[] = [ - { columnIds: [null, null, undefined, undefined], invalidKeys: [], testDescription: 'does not require column IDs' }, - { columnIds: [null, 'my-id', undefined, undefined], invalidKeys: ['missingColumnId'], testDescription: 'requires column IDs for all columns if a column ID is defined for at least one' }, - { columnIds: ['my-id-1', 'my-id-2', 'my-id-3'], invalidKeys: [], testDescription: 'unique defined IDs are valid' }, - { columnIds: ['my-id-1', 'my-id-2', 'my-id-2'], invalidKeys: ['duplicateColumnId'], testDescription: 'duplicate column IDs is invalid' }, - { columnIds: ['my-id-1', 'my-id-2', 'my-id-2', undefined], invalidKeys: ['missingColumnId', 'duplicateColumnId'], testDescription: 'reports multiple column ID validation errors' }, - { columnIds: ['my-id-1', 'my-id-2', ''], invalidKeys: [], testDescription: 'empty string is a valid column ID' }, - { columnIds: ['my-id-1', '', ''], invalidKeys: ['duplicateColumnId'], testDescription: 'detects duplicate empty string column IDs' }, - { columnIds: [null, undefined, ''], invalidKeys: ['missingColumnId'], testDescription: 'requires column IDs for all columns if one is set to empty string' }, + const columnConfigurations: { + columnIds: (string | undefined | null)[], + invalidKeys: (keyof TableValidity)[], + testDescription: string + }[] = [ + { + columnIds: [null, null, undefined, undefined], + invalidKeys: [], + testDescription: 'does not require column IDs' + }, + { + columnIds: [null, 'my-id', undefined, undefined], + invalidKeys: ['missingColumnId'], + testDescription: + 'requires column IDs for all columns if a column ID is defined for at least one' + }, + { + columnIds: ['my-id-1', 'my-id-2', 'my-id-3'], + invalidKeys: [], + testDescription: 'unique defined IDs are valid' + }, + { + columnIds: ['my-id-1', 'my-id-2', 'my-id-2'], + invalidKeys: ['duplicateColumnId'], + testDescription: 'duplicate column IDs is invalid' + }, + { + columnIds: ['my-id-1', 'my-id-2', 'my-id-2', undefined], + invalidKeys: ['missingColumnId', 'duplicateColumnId'], + testDescription: 'reports multiple column ID validation errors' + }, + { + columnIds: ['my-id-1', 'my-id-2', ''], + invalidKeys: [], + testDescription: 'empty string is a valid column ID' + }, + { + columnIds: ['my-id-1', '', ''], + invalidKeys: ['duplicateColumnId'], + testDescription: 'detects duplicate empty string column IDs' + }, + { + columnIds: [null, undefined, ''], + invalidKeys: ['missingColumnId'], + testDescription: + 'requires column IDs for all columns if one is set to empty string' + } ]; for (const columnConfiguration of columnConfigurations) { // eslint-disable-next-line @typescript-eslint/no-loop-func it(columnConfiguration.testDescription, () => { - const isValid = validator.validateColumnIds(columnConfiguration.columnIds); - expect(isValid).toBe(columnConfiguration.invalidKeys.length === 0); + const isValid = validator.validateColumnIds( + columnConfiguration.columnIds + ); + expect(isValid).toBe( + columnConfiguration.invalidKeys.length === 0 + ); validateValidity(columnConfiguration.invalidKeys); }); } @@ -237,21 +293,43 @@ describe('TableValidator', () => { { stringField: true, numberField: 12 } ]; - const recordIdsAreValid = validator.validateRecordIds(data, 'stringField'); + const recordIdsAreValid = validator.validateRecordIds( + data, + 'stringField' + ); expect(recordIdsAreValid).toBeFalse(); - validateValidity(['missingRecordId', 'duplicateRecordId', 'invalidRecordId']); - - const columnIdsAreValid = validator.validateColumnIds(['id-1', 'id-2', 'id-3']); + validateValidity([ + 'missingRecordId', + 'duplicateRecordId', + 'invalidRecordId' + ]); + + const columnIdsAreValid = validator.validateColumnIds([ + 'id-1', + 'id-2', + 'id-3' + ]); expect(columnIdsAreValid).toBeTrue(); - validateValidity(['missingRecordId', 'duplicateRecordId', 'invalidRecordId']); + validateValidity([ + 'missingRecordId', + 'duplicateRecordId', + 'invalidRecordId' + ]); }); it('invalid column IDs stay invalid when validating record IDs', () => { - const columnIdsAreValid = validator.validateColumnIds(['id-1', 'id-1', undefined]); + const columnIdsAreValid = validator.validateColumnIds([ + 'id-1', + 'id-1', + undefined + ]); expect(columnIdsAreValid).toBeFalse(); validateValidity(['missingColumnId', 'duplicateColumnId']); - const recordIdsAreValid = validator.validateRecordIds([], undefined); + const recordIdsAreValid = validator.validateRecordIds( + [], + undefined + ); expect(recordIdsAreValid).toBeTrue(); validateValidity(['missingColumnId', 'duplicateColumnId']); }); diff --git a/packages/nimble-components/src/table/models/table-validator.ts b/packages/nimble-components/src/table/models/table-validator.ts index 76e1d71b9c..941bf0c5c3 100644 --- a/packages/nimble-components/src/table/models/table-validator.ts +++ b/packages/nimble-components/src/table/models/table-validator.ts @@ -64,11 +64,15 @@ export class TableValidator<TData extends TableRecord> { ); } - public validateColumnIds(columnIds: (string | null | undefined)[]): boolean { + public validateColumnIds( + columnIds: (string | null | undefined)[] + ): boolean { this.missingColumnId = false; this.duplicateColumnId = false; - const anyColumnsHaveIds = columnIds.some(columnId => columnId !== undefined && columnId !== null); + const anyColumnsHaveIds = columnIds.some( + columnId => columnId !== undefined && columnId !== null + ); if (!anyColumnsHaveIds) { return true; @@ -87,9 +91,6 @@ export class TableValidator<TData extends TableRecord> { idSet.add(columnId); } - return ( - !this.missingColumnId - && !this.duplicateColumnId - ); + return !this.missingColumnId && !this.duplicateColumnId; } } diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index 22ff9d9542..f4606841c3 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -1,10 +1,4 @@ -import { - children, - html, - ref, - repeat, - when -} from '@microsoft/fast-element'; +import { children, html, ref, repeat, when } from '@microsoft/fast-element'; import { DesignSystem } from '@microsoft/fast-foundation'; import type { VirtualItem } from '@tanstack/virtual-core'; import type { Table } from '.'; From 641af9c533516ca9749f6ea0d79803ed00231d77 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 7 Feb 2023 10:08:35 -0600 Subject: [PATCH 10/23] remove old test (merge issue) --- .../src/table/models/table-validator.spec.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index 3126a5b9ec..a01445a104 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -52,17 +52,6 @@ describe('TableValidator', () => { validateValidity([]); }); - it('setting `null` field for ID is valid', () => { - const data = [ - { stringField: 'value-1', numberField: 10 }, - { stringField: 'value-2', numberField: 11 } - ]; - - const isValid = validator.validateRecordIds(data, null); - expect(isValid).toBeTrue(); - validateValidity([]); - }); - it('setting data with duplicate IDs is invalid', () => { const data = [ { stringField: 'value-1', numberField: 10 }, From ab84077dccb56cb719422c39864d89e721918eda Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 7 Feb 2023 11:48:07 -0600 Subject: [PATCH 11/23] clean up + don't allow null column ids --- .../src/table-column/base/index.ts | 2 +- packages/nimble-components/src/table/index.ts | 18 ++++++++-------- .../src/table/models/table-validator.spec.ts | 21 +++++-------------- .../src/table/models/table-validator.ts | 8 +++---- .../src/table/tests/table.stories.ts | 8 +++---- 5 files changed, 22 insertions(+), 35 deletions(-) diff --git a/packages/nimble-components/src/table-column/base/index.ts b/packages/nimble-components/src/table-column/base/index.ts index 6696c5d699..4e578f4763 100644 --- a/packages/nimble-components/src/table-column/base/index.ts +++ b/packages/nimble-components/src/table-column/base/index.ts @@ -14,7 +14,7 @@ export abstract class TableColumn< TColumnConfig = unknown > extends FoundationElement { @attr({ attribute: 'column-id' }) - public columnId?: string | null; + public columnId?: string; /** * The template to use to render the cell content for the column diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index c1338214dd..9da901f06e 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -90,11 +90,7 @@ export class Table< public override connectedCallback(): void { super.connectedCallback(); this.virtualizer.connectedCallback(); - - this.tableValidator.validateColumnIds( - this.columns.map(x => x.columnId) - ); - this.canRenderRows = this.checkValidity(); + this.validateColumnIds(); } public override disconnectedCallback(): void { @@ -122,10 +118,7 @@ export class Table< return; } - this.tableValidator.validateColumnIds( - this.columns.map(x => x.columnId) - ); - this.canRenderRows = this.checkValidity(); + this.validateColumnIds(); } private childItemsChanged( @@ -137,6 +130,13 @@ export class Table< .map(x => x as TableColumn); } + private validateColumnIds(): void { + this.tableValidator.validateColumnIds( + this.columns.map(x => x.columnId) + ); + this.canRenderRows = this.checkValidity(); + } + private setTableData(newData: readonly TData[]): void { const data = newData.map(record => { return { ...record }; diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index a01445a104..6246610af0 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -210,17 +210,17 @@ describe('TableValidator', () => { describe('column ID validation', () => { const columnConfigurations: { - columnIds: (string | undefined | null)[], + columnIds: (string | undefined)[], invalidKeys: (keyof TableValidity)[], testDescription: string }[] = [ { - columnIds: [null, null, undefined, undefined], + columnIds: [undefined, ''], invalidKeys: [], testDescription: 'does not require column IDs' }, { - columnIds: [null, 'my-id', undefined, undefined], + columnIds: ['my-id', undefined, undefined], invalidKeys: ['missingColumnId'], testDescription: 'requires column IDs for all columns if a column ID is defined for at least one' @@ -241,20 +241,9 @@ describe('TableValidator', () => { testDescription: 'reports multiple column ID validation errors' }, { - columnIds: ['my-id-1', 'my-id-2', ''], - invalidKeys: [], - testDescription: 'empty string is a valid column ID' - }, - { - columnIds: ['my-id-1', '', ''], - invalidKeys: ['duplicateColumnId'], - testDescription: 'detects duplicate empty string column IDs' - }, - { - columnIds: [null, undefined, ''], + columnIds: ['my-id-1', ''], invalidKeys: ['missingColumnId'], - testDescription: - 'requires column IDs for all columns if one is set to empty string' + testDescription: 'does not allow empty string as a defined column ID' } ]; diff --git a/packages/nimble-components/src/table/models/table-validator.ts b/packages/nimble-components/src/table/models/table-validator.ts index 6b6bd17dd5..ad6cbdc49b 100644 --- a/packages/nimble-components/src/table/models/table-validator.ts +++ b/packages/nimble-components/src/table/models/table-validator.ts @@ -65,14 +65,12 @@ export class TableValidator<TData extends TableRecord> { } public validateColumnIds( - columnIds: (string | null | undefined)[] + columnIds: (string | undefined)[] ): boolean { this.missingColumnId = false; this.duplicateColumnId = false; - const anyColumnsHaveIds = columnIds.some( - columnId => columnId !== undefined && columnId !== null - ); + const anyColumnsHaveIds = columnIds.some(id => id); if (!anyColumnsHaveIds) { return true; @@ -80,7 +78,7 @@ export class TableValidator<TData extends TableRecord> { const idSet = new Set<string>(); for (const columnId of columnIds) { - if (typeof columnId !== 'string') { + if (!columnId) { this.missingColumnId = true; continue; } diff --git a/packages/nimble-components/src/table/tests/table.stories.ts b/packages/nimble-components/src/table/tests/table.stories.ts index 6481d9aad8..5cd3030052 100644 --- a/packages/nimble-components/src/table/tests/table.stories.ts +++ b/packages/nimble-components/src/table/tests/table.stories.ts @@ -127,10 +127,10 @@ const metadata: Meta<TableArgs> = { id-field-name="${x => dataSetIdFieldNames[x.data]}" data-unused="${x => x.updateData(x)}" > - <nimble-table-column-text field-name="firstName" placeholder="no value" column-id="foo">First Name</nimble-table-column-text> - <nimble-table-column-text field-name="lastName" placeholder="no value" column-id="food">Last Name</nimble-table-column-text> - <nimble-table-column-text field-name="favoriteColor" placeholder="no value" column-id="bar">Favorite Color</nimble-table-column-text> - <nimble-table-column-text field-name="quote" placeholder="no value" column-id="baz">Quote</nimble-table-column-text> + <nimble-table-column-text field-name="firstName" placeholder="no value" column-id="first-name-column">First Name</nimble-table-column-text> + <nimble-table-column-text field-name="lastName" placeholder="no value" column-id="last-name-column">Last Name</nimble-table-column-text> + <nimble-table-column-text field-name="favoriteColor" placeholder="no value" column-id="favorite-color-column">Favorite Color</nimble-table-column-text> + <nimble-table-column-text field-name="quote" placeholder="no value" column-id="quote-column">Quote</nimble-table-column-text> </nimble-table> <style class="code-hide"> #usage-warning { From 1e1c59eb5b934c90aa0a503485e07efe0b960324 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 7 Feb 2023 11:50:04 -0600 Subject: [PATCH 12/23] format --- .../src/table/models/table-validator.spec.ts | 3 ++- .../nimble-components/src/table/models/table-validator.ts | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index 6246610af0..9dd03cabb9 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -243,7 +243,8 @@ describe('TableValidator', () => { { columnIds: ['my-id-1', ''], invalidKeys: ['missingColumnId'], - testDescription: 'does not allow empty string as a defined column ID' + testDescription: + 'does not allow empty string as a defined column ID' } ]; diff --git a/packages/nimble-components/src/table/models/table-validator.ts b/packages/nimble-components/src/table/models/table-validator.ts index ad6cbdc49b..36c1221eec 100644 --- a/packages/nimble-components/src/table/models/table-validator.ts +++ b/packages/nimble-components/src/table/models/table-validator.ts @@ -64,9 +64,7 @@ export class TableValidator<TData extends TableRecord> { ); } - public validateColumnIds( - columnIds: (string | undefined)[] - ): boolean { + public validateColumnIds(columnIds: (string | undefined)[]): boolean { this.missingColumnId = false; this.duplicateColumnId = false; From 77c49eb92898a09977868c3cf246c8f089be59a4 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 7 Feb 2023 11:51:46 -0600 Subject: [PATCH 13/23] remove null as type in Angular --- .../table-column/text/nimble-table-column-text.directive.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts b/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts index 66416fa0bf..05bb21f9be 100644 --- a/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts +++ b/angular-workspace/projects/ni/nimble-angular/src/directives/table-column/text/nimble-table-column-text.directive.ts @@ -10,13 +10,13 @@ export type { TableColumnText }; selector: 'nimble-table-column-text' }) export class NimbleTableColumnTextDirective { - public get columnId(): string | null | undefined { + public get columnId(): string | undefined { return this.elementRef.nativeElement.columnId; } // Renaming because property should have camel casing, but attribute should not // eslint-disable-next-line @angular-eslint/no-input-rename - @Input('column-id') public set columnId(value: string | null | undefined) { + @Input('column-id') public set columnId(value: string | undefined) { this.renderer.setProperty(this.elementRef.nativeElement, 'columnId', value); } From 7e4997552860ddc0ca127f16ca57d2fb2ea3d805 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:26:46 -0600 Subject: [PATCH 14/23] simplify --- packages/nimble-components/src/table/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index 9da901f06e..ce3c06b4e9 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -126,8 +126,7 @@ export class Table< next: Element[] ): void { this.columns = next - .filter(x => x instanceof TableColumn) - .map(x => x as TableColumn); + .filter((x): x is TableColumn => x instanceof TableColumn); } private validateColumnIds(): void { From d019e9e763453f36dff0f3bd33c6f57848ea83b5 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Wed, 8 Feb 2023 14:06:32 -0600 Subject: [PATCH 15/23] change observation pattern --- packages/nimble-components/src/table/index.ts | 41 +++++++++++++------ .../nimble-components/src/table/template.ts | 25 ++++++----- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index ce3c06b4e9..1d721cd1b9 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -1,4 +1,4 @@ -import { attr, observable } from '@microsoft/fast-element'; +import { attr, Observable, observable, Notifier } from '@microsoft/fast-element'; import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; import { ColumnDef as TanStackColumnDef, @@ -9,12 +9,12 @@ import { getCoreRowModel as tanStackGetCoreRowModel, TableOptionsResolved as TanStackTableOptionsResolved } from '@tanstack/table-core'; -import { TableColumn } from '../table-column/base'; import { TableValidator } from './models/table-validator'; import { styles } from './styles'; import { template } from './template'; import type { TableRecord, TableRowState, TableValidity } from './types'; import { Virtualizer } from './models/virtualizer'; +import { TableColumn } from '../table-column/base'; declare global { interface HTMLElementTagNameMap { @@ -38,7 +38,7 @@ export class Table< public tableData: TableRowState<TData>[] = []; @observable - public columns: TableColumn[] = []; + public readonly columns: TableColumn[] = []; /** * @internal @@ -55,9 +55,6 @@ export class Table< */ public readonly viewport!: HTMLElement; - @observable - public childItems: Element[] = []; - /** * @internal */ @@ -66,6 +63,7 @@ export class Table< private readonly table: TanStackTable<TData>; private options: TanStackTableOptionsResolved<TData>; private readonly tableValidator = new TableValidator(); + private notifiers: Notifier[] = []; public constructor() { super(); @@ -90,6 +88,7 @@ export class Table< public override connectedCallback(): void { super.connectedCallback(); this.virtualizer.connectedCallback(); + this.observeColumns(); this.validateColumnIds(); } @@ -101,6 +100,19 @@ export class Table< return this.tableValidator.isValid(); } + /** + * @internal + */ + // 'handleChange' is an API exposed by FAST that we need to implement. Disable lint rules caused by its signature. + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + public handleChange(source: any, args: any): void { + if (source instanceof TableColumn) { + if (args === 'columnId') { + this.validateColumnIds(); + } + } + } + private idFieldNameChanged( _prev: string | undefined, _next: string | undefined @@ -118,15 +130,20 @@ export class Table< return; } + this.observeColumns(); this.validateColumnIds(); } - private childItemsChanged( - _prev: undefined | Element[], - next: Element[] - ): void { - this.columns = next - .filter((x): x is TableColumn => x instanceof TableColumn); + private observeColumns(): void { + this.notifiers.forEach(notifier => { + notifier.unsubscribe(this); + }); + this.notifiers = []; + + for (const column of this.columns) { + const notifier = Observable.getNotifier(column); + notifier.subscribe(this); + } } private validateColumnIds(): void { diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index f4606841c3..da64b703da 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -1,22 +1,25 @@ -import { children, html, ref, repeat, when } from '@microsoft/fast-element'; +import { children, ElementsFilter, html, ref, repeat, when } from '@microsoft/fast-element'; import { DesignSystem } from '@microsoft/fast-foundation'; import type { VirtualItem } from '@tanstack/virtual-core'; import type { Table } from '.'; import { TableHeader } from './components/header'; import { TableRow } from './components/row'; -import type { TableColumn } from '../table-column/base'; +import { TableColumn } from '../table-column/base'; + +const isTableColumn = (): ElementsFilter => { + const filter: ElementsFilter = ( + value: Node, + _: number, + __: Node[] + ): boolean => { + return value instanceof TableColumn; + }; + return filter; +}; // prettier-ignore export const template = html<Table>` - <template - role="table" - ${children({ - property: 'childItems', - attributeFilter: ['column-id'], - subtree: true, - selector: '*' - })} - > + <template role="table" ${children({ property: 'columns', filter: isTableColumn() })}> <div class="table-container"> <div role="rowgroup" class="header-container" style="margin-right: ${x => x.virtualizer.headerContainerMarginRight}px;"> <div class="header-row" role="row"> From f5590a7770f9cbafa36b7324ac62c795ddebe15a Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Wed, 8 Feb 2023 14:26:48 -0600 Subject: [PATCH 16/23] Update table-validator.spec.ts --- .../src/table/models/table-validator.spec.ts | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index 9dd03cabb9..ad6400e6cc 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -8,25 +8,10 @@ describe('TableValidator', () => { validator = new TableValidator(); }); - function validateValidity(invalidKeys: (keyof TableValidity)[]): void { - expect(validator.isValid()).toEqual(invalidKeys.length === 0); - - const validity = validator.getValidity(); - expect(validity.duplicateRecordId).toBe( - invalidKeys.some(x => x === 'duplicateRecordId') - ); - expect(validity.missingRecordId).toBe( - invalidKeys.some(x => x === 'missingRecordId') - ); - expect(validity.invalidRecordId).toBe( - invalidKeys.some(x => x === 'invalidRecordId') - ); - expect(validity.duplicateColumnId).toBe( - invalidKeys.some(x => x === 'duplicateColumnId') - ); - expect(validity.missingColumnId).toBe( - invalidKeys.some(x => x === 'missingColumnId') - ); + function getInvalidKeys(tableValidator: TableValidator<TableRecord>): string[] { + return Object.entries(tableValidator) + .filter(([_, value]) => value) + .map(([key, _]) => key); } describe('record ID validation', () => { @@ -38,7 +23,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeTrue(); - validateValidity([]); + expect(validator.isValid()).toBeTrue(); + expect(getInvalidKeys(validator)).toEqual([]); }); it('setting `undefined` field for ID is valid', () => { @@ -49,7 +35,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, undefined); expect(isValid).toBeTrue(); - validateValidity([]); + expect(validator.isValid()).toBeTrue(); + expect(getInvalidKeys(validator)).toEqual([]); }); it('setting data with duplicate IDs is invalid', () => { @@ -60,7 +47,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); - validateValidity(['duplicateRecordId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['duplicateRecordId'])); }); it('setting data with invalid ID value type is invalid', () => { @@ -71,7 +59,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'numberField'); expect(isValid).toBeFalse(); - validateValidity(['invalidRecordId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['invalidRecordId'])); }); it('setting data with empty ID value is valid', () => { @@ -82,7 +71,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeTrue(); - validateValidity([]); + expect(validator.isValid()).toBeTrue(); + expect(getInvalidKeys(validator)).toEqual([]); }); it('setting data with undefined ID value is invalid', () => { @@ -93,7 +83,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); - validateValidity(['invalidRecordId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['invalidRecordId'])); }); it('setting data with null ID value is invalid', () => { @@ -104,7 +95,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); - validateValidity(['invalidRecordId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['invalidRecordId'])); }); it('setting data with missing IDs is invalid', () => { @@ -115,7 +107,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'missingField'); expect(isValid).toBeFalse(); - validateValidity(['missingRecordId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['missingRecordId'])); }); it('multiple errors are reported during validation', () => { @@ -129,11 +122,12 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); - validateValidity([ + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents([ 'missingRecordId', 'duplicateRecordId', 'invalidRecordId' - ]); + ])); }); it('setting ID field name to undefined makes configuration valid', () => { @@ -191,7 +185,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, ''); expect(isValid).toBeTrue(); - validateValidity([]); + expect(validator.isValid()).toBeTrue(); + expect(getInvalidKeys(validator)).toEqual([]); }); it('validation occurs when ID field name is an empty string', () => { @@ -204,7 +199,8 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, ''); expect(isValid).toBeFalse(); - validateValidity(['duplicateRecordId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['duplicateRecordId'])); }); }); @@ -257,7 +253,8 @@ describe('TableValidator', () => { expect(isValid).toBe( columnConfiguration.invalidKeys.length === 0 ); - validateValidity(columnConfiguration.invalidKeys); + expect(validator.isValid()).toBe(columnConfiguration.invalidKeys.length === 0); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(columnConfiguration.invalidKeys)); }); } }); @@ -277,11 +274,12 @@ describe('TableValidator', () => { 'stringField' ); expect(recordIdsAreValid).toBeFalse(); - validateValidity([ + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents([ 'missingRecordId', 'duplicateRecordId', 'invalidRecordId' - ]); + ])); const columnIdsAreValid = validator.validateColumnIds([ 'id-1', @@ -289,11 +287,12 @@ describe('TableValidator', () => { 'id-3' ]); expect(columnIdsAreValid).toBeTrue(); - validateValidity([ + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents([ 'missingRecordId', 'duplicateRecordId', 'invalidRecordId' - ]); + ])); }); it('invalid column IDs stay invalid when validating record IDs', () => { @@ -303,14 +302,16 @@ describe('TableValidator', () => { undefined ]); expect(columnIdsAreValid).toBeFalse(); - validateValidity(['missingColumnId', 'duplicateColumnId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['missingColumnId', 'duplicateColumnId'])); const recordIdsAreValid = validator.validateRecordIds( [], undefined ); expect(recordIdsAreValid).toBeTrue(); - validateValidity(['missingColumnId', 'duplicateColumnId']); + expect(validator.isValid()).toBeFalse(); + expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['missingColumnId', 'duplicateColumnId'])); }); }); }); From 646bec5d208556e55301d512e0d8143a039d09db Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Wed, 8 Feb 2023 14:28:52 -0600 Subject: [PATCH 17/23] format --- packages/nimble-components/src/table/index.ts | 7 +- .../src/table/models/table-validator.spec.ts | 88 +++++++++++++------ .../nimble-components/src/table/template.ts | 9 +- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index 1d721cd1b9..d42efb2e43 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -1,4 +1,9 @@ -import { attr, Observable, observable, Notifier } from '@microsoft/fast-element'; +import { + attr, + Observable, + observable, + Notifier +} from '@microsoft/fast-element'; import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; import { ColumnDef as TanStackColumnDef, diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index ad6400e6cc..db69de06a6 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -8,7 +8,9 @@ describe('TableValidator', () => { validator = new TableValidator(); }); - function getInvalidKeys(tableValidator: TableValidator<TableRecord>): string[] { + function getInvalidKeys( + tableValidator: TableValidator<TableRecord> + ): string[] { return Object.entries(tableValidator) .filter(([_, value]) => value) .map(([key, _]) => key); @@ -48,7 +50,9 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['duplicateRecordId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents(['duplicateRecordId']) + ); }); it('setting data with invalid ID value type is invalid', () => { @@ -60,7 +64,9 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'numberField'); expect(isValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['invalidRecordId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents(['invalidRecordId']) + ); }); it('setting data with empty ID value is valid', () => { @@ -84,7 +90,9 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['invalidRecordId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents(['invalidRecordId']) + ); }); it('setting data with null ID value is invalid', () => { @@ -96,7 +104,9 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['invalidRecordId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents(['invalidRecordId']) + ); }); it('setting data with missing IDs is invalid', () => { @@ -108,7 +118,9 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'missingField'); expect(isValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['missingRecordId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents(['missingRecordId']) + ); }); it('multiple errors are reported during validation', () => { @@ -123,11 +135,13 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, 'stringField'); expect(isValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents([ - 'missingRecordId', - 'duplicateRecordId', - 'invalidRecordId' - ])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents([ + 'missingRecordId', + 'duplicateRecordId', + 'invalidRecordId' + ]) + ); }); it('setting ID field name to undefined makes configuration valid', () => { @@ -200,7 +214,9 @@ describe('TableValidator', () => { const isValid = validator.validateRecordIds(data, ''); expect(isValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['duplicateRecordId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents(['duplicateRecordId']) + ); }); }); @@ -253,8 +269,14 @@ describe('TableValidator', () => { expect(isValid).toBe( columnConfiguration.invalidKeys.length === 0 ); - expect(validator.isValid()).toBe(columnConfiguration.invalidKeys.length === 0); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(columnConfiguration.invalidKeys)); + expect(validator.isValid()).toBe( + columnConfiguration.invalidKeys.length === 0 + ); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents( + columnConfiguration.invalidKeys + ) + ); }); } }); @@ -275,11 +297,13 @@ describe('TableValidator', () => { ); expect(recordIdsAreValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents([ - 'missingRecordId', - 'duplicateRecordId', - 'invalidRecordId' - ])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents([ + 'missingRecordId', + 'duplicateRecordId', + 'invalidRecordId' + ]) + ); const columnIdsAreValid = validator.validateColumnIds([ 'id-1', @@ -288,11 +312,13 @@ describe('TableValidator', () => { ]); expect(columnIdsAreValid).toBeTrue(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents([ - 'missingRecordId', - 'duplicateRecordId', - 'invalidRecordId' - ])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents([ + 'missingRecordId', + 'duplicateRecordId', + 'invalidRecordId' + ]) + ); }); it('invalid column IDs stay invalid when validating record IDs', () => { @@ -303,7 +329,12 @@ describe('TableValidator', () => { ]); expect(columnIdsAreValid).toBeFalse(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['missingColumnId', 'duplicateColumnId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents([ + 'missingColumnId', + 'duplicateColumnId' + ]) + ); const recordIdsAreValid = validator.validateRecordIds( [], @@ -311,7 +342,12 @@ describe('TableValidator', () => { ); expect(recordIdsAreValid).toBeTrue(); expect(validator.isValid()).toBeFalse(); - expect(getInvalidKeys(validator)).toEqual(jasmine.arrayWithExactContents(['missingColumnId', 'duplicateColumnId'])); + expect(getInvalidKeys(validator)).toEqual( + jasmine.arrayWithExactContents([ + 'missingColumnId', + 'duplicateColumnId' + ]) + ); }); }); }); diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index da64b703da..26c821d1e7 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -1,4 +1,11 @@ -import { children, ElementsFilter, html, ref, repeat, when } from '@microsoft/fast-element'; +import { + children, + ElementsFilter, + html, + ref, + repeat, + when +} from '@microsoft/fast-element'; import { DesignSystem } from '@microsoft/fast-foundation'; import type { VirtualItem } from '@tanstack/virtual-core'; import type { Table } from '.'; From e7072b3ce7226a555ff4270b5614f9c08c62f6d8 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Wed, 8 Feb 2023 14:33:16 -0600 Subject: [PATCH 18/23] remove observers on disconnect --- packages/nimble-components/src/table/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index d42efb2e43..79e97e8739 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -14,12 +14,12 @@ import { getCoreRowModel as tanStackGetCoreRowModel, TableOptionsResolved as TanStackTableOptionsResolved } from '@tanstack/table-core'; +import { TableColumn } from '../table-column/base'; import { TableValidator } from './models/table-validator'; import { styles } from './styles'; import { template } from './template'; import type { TableRecord, TableRowState, TableValidity } from './types'; import { Virtualizer } from './models/virtualizer'; -import { TableColumn } from '../table-column/base'; declare global { interface HTMLElementTagNameMap { @@ -99,6 +99,7 @@ export class Table< public override disconnectedCallback(): void { this.virtualizer.disconnectedCallback(); + this.removeColumnObservers(); } public checkValidity(): boolean { @@ -139,11 +140,15 @@ export class Table< this.validateColumnIds(); } - private observeColumns(): void { + private removeColumnObservers(): void { this.notifiers.forEach(notifier => { notifier.unsubscribe(this); }); this.notifiers = []; + } + + private observeColumns(): void { + this.removeColumnObservers(); for (const column of this.columns) { const notifier = Observable.getNotifier(column); From 2f2c549ddcab60f5a4c5b145158c7a8382310be9 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Thu, 9 Feb 2023 11:40:20 -0600 Subject: [PATCH 19/23] create column base class --- .../Components/NimbleTableColumnText.razor | 1 + .../Components/NimbleTableColumnText.razor.cs | 8 +------- .../nimble-blazor/NimbleBlazor/NimbleTableColumn.cs | 12 ++++++++++++ 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 packages/nimble-blazor/NimbleBlazor/NimbleTableColumn.cs diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor index 2ed123db5f..79a67309c8 100644 --- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor +++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor @@ -1,4 +1,5 @@ @namespace NimbleBlazor +@inherits NimbleTableColumn <nimble-table-column-text column-id="@ColumnId" field-name="@FieldName" placeholder="@Placeholder"> @ChildContent diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs index a7aef0a30c..53e508ee90 100644 --- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs +++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTableColumnText.razor.cs @@ -3,14 +3,8 @@ namespace NimbleBlazor; -public partial class NimbleTableColumnText : ComponentBase +public partial class NimbleTableColumnText : NimbleTableColumn { - /// <summary> - /// The ID of the column of a <see cref="NimbleTable{TData}"/> - /// </summary> - [Parameter] - public string? ColumnId { get; set; } - /// <summary> /// Gets or sets the field in the element representing a row of data in a <see cref="NimbleTable{TData}"/>to display /// </summary> diff --git a/packages/nimble-blazor/NimbleBlazor/NimbleTableColumn.cs b/packages/nimble-blazor/NimbleBlazor/NimbleTableColumn.cs new file mode 100644 index 0000000000..bc26a16ced --- /dev/null +++ b/packages/nimble-blazor/NimbleBlazor/NimbleTableColumn.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Components; + +namespace NimbleBlazor; + +public abstract class NimbleTableColumn : ComponentBase +{ + /// <summary> + /// The ID of the column of a <see cref="NimbleTable{TData}"/> + /// </summary> + [Parameter] + public string? ColumnId { get; set; } +} From b212100637721d5dfcbc70e0d0994cc9b71a9a4f Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 10 Feb 2023 11:35:01 -0600 Subject: [PATCH 20/23] PR feedback --- packages/nimble-components/src/table/index.ts | 26 +++++++----- .../src/table/models/table-validator.spec.ts | 42 +++++++++++-------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index 79e97e8739..fe88f9d9e2 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -68,7 +68,7 @@ export class Table< private readonly table: TanStackTable<TData>; private options: TanStackTableOptionsResolved<TData>; private readonly tableValidator = new TableValidator(); - private notifiers: Notifier[] = []; + private columnNotifiers: Notifier[] = []; public constructor() { super(); @@ -93,11 +93,11 @@ export class Table< public override connectedCallback(): void { super.connectedCallback(); this.virtualizer.connectedCallback(); - this.observeColumns(); - this.validateColumnIds(); + this.validateAndObserveColumns(); } public override disconnectedCallback(): void { + super.disconnectedCallback(); this.virtualizer.disconnectedCallback(); this.removeColumnObservers(); } @@ -108,10 +108,12 @@ export class Table< /** * @internal + * + * The event handler that is called when a notifier detects a change. Notifiers are added + * to each column, so `source` is expected to be an instance of `TableColumn`, and `args` + * is the string name of the property that changed on that column. */ - // 'handleChange' is an API exposed by FAST that we need to implement. Disable lint rules caused by its signature. - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - public handleChange(source: any, args: any): void { + public handleChange(source: unknown, args: unknown): void { if (source instanceof TableColumn) { if (args === 'columnId') { this.validateColumnIds(); @@ -136,24 +138,26 @@ export class Table< return; } - this.observeColumns(); - this.validateColumnIds(); + this.validateAndObserveColumns(); } private removeColumnObservers(): void { - this.notifiers.forEach(notifier => { + this.columnNotifiers.forEach(notifier => { notifier.unsubscribe(this); }); - this.notifiers = []; + this.columnNotifiers = []; } - private observeColumns(): void { + private validateAndObserveColumns(): void { this.removeColumnObservers(); for (const column of this.columns) { const notifier = Observable.getNotifier(column); notifier.subscribe(this); + this.columnNotifiers.push(notifier); } + + this.validateColumnIds(); } private validateColumnIds(): void { diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index db69de06a6..e5ed64559d 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -1,5 +1,6 @@ import type { TableRecord, TableValidity } from '../types'; import { TableValidator } from './table-validator'; +import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized'; describe('TableValidator', () => { let validator: TableValidator<TableRecord>; @@ -223,56 +224,63 @@ describe('TableValidator', () => { describe('column ID validation', () => { const columnConfigurations: { columnIds: (string | undefined)[], + isValid: boolean, invalidKeys: (keyof TableValidity)[], - testDescription: string + name: string }[] = [ { columnIds: [undefined, ''], + isValid: true, invalidKeys: [], - testDescription: 'does not require column IDs' + name: 'does not require column IDs' }, { columnIds: ['my-id', undefined, undefined], + isValid: false, invalidKeys: ['missingColumnId'], - testDescription: + name: 'requires column IDs for all columns if a column ID is defined for at least one' }, { columnIds: ['my-id-1', 'my-id-2', 'my-id-3'], + isValid: true, invalidKeys: [], - testDescription: 'unique defined IDs are valid' + name: 'unique defined IDs are valid' }, { columnIds: ['my-id-1', 'my-id-2', 'my-id-2'], + isValid: false, invalidKeys: ['duplicateColumnId'], - testDescription: 'duplicate column IDs is invalid' + name: 'duplicate column IDs is invalid' }, { columnIds: ['my-id-1', 'my-id-2', 'my-id-2', undefined], + isValid: false, invalidKeys: ['missingColumnId', 'duplicateColumnId'], - testDescription: 'reports multiple column ID validation errors' + name: 'reports multiple column ID validation errors' }, { columnIds: ['my-id-1', ''], + isValid: false, invalidKeys: ['missingColumnId'], - testDescription: + name: 'does not allow empty string as a defined column ID' } ]; + const focused: string[] = []; + const disabled: string[] = []; for (const columnConfiguration of columnConfigurations) { - // eslint-disable-next-line @typescript-eslint/no-loop-func - it(columnConfiguration.testDescription, () => { - const isValid = validator.validateColumnIds( + const specType = getSpecTypeByNamedList(columnConfiguration, focused, disabled); + specType(columnConfiguration.name, () => { + const tableValidator = new TableValidator(); + const isValid = tableValidator.validateColumnIds( columnConfiguration.columnIds ); - expect(isValid).toBe( - columnConfiguration.invalidKeys.length === 0 - ); - expect(validator.isValid()).toBe( - columnConfiguration.invalidKeys.length === 0 - ); - expect(getInvalidKeys(validator)).toEqual( + + expect(isValid).toBe(columnConfiguration.isValid); + expect(tableValidator.isValid()).toBe(columnConfiguration.invalidKeys.length === 0); + expect(getInvalidKeys(tableValidator)).toEqual( jasmine.arrayWithExactContents( columnConfiguration.invalidKeys ) From 3d958f47ba2efef64b2e613ee127ba3d65664e7c Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Fri, 10 Feb 2023 11:37:01 -0600 Subject: [PATCH 21/23] format --- .../src/table/models/table-validator.spec.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/nimble-components/src/table/models/table-validator.spec.ts b/packages/nimble-components/src/table/models/table-validator.spec.ts index e5ed64559d..eee0c02f71 100644 --- a/packages/nimble-components/src/table/models/table-validator.spec.ts +++ b/packages/nimble-components/src/table/models/table-validator.spec.ts @@ -238,8 +238,7 @@ describe('TableValidator', () => { columnIds: ['my-id', undefined, undefined], isValid: false, invalidKeys: ['missingColumnId'], - name: - 'requires column IDs for all columns if a column ID is defined for at least one' + name: 'requires column IDs for all columns if a column ID is defined for at least one' }, { columnIds: ['my-id-1', 'my-id-2', 'my-id-3'], @@ -263,15 +262,18 @@ describe('TableValidator', () => { columnIds: ['my-id-1', ''], isValid: false, invalidKeys: ['missingColumnId'], - name: - 'does not allow empty string as a defined column ID' + name: 'does not allow empty string as a defined column ID' } ]; const focused: string[] = []; const disabled: string[] = []; for (const columnConfiguration of columnConfigurations) { - const specType = getSpecTypeByNamedList(columnConfiguration, focused, disabled); + const specType = getSpecTypeByNamedList( + columnConfiguration, + focused, + disabled + ); specType(columnConfiguration.name, () => { const tableValidator = new TableValidator(); const isValid = tableValidator.validateColumnIds( @@ -279,7 +281,9 @@ describe('TableValidator', () => { ); expect(isValid).toBe(columnConfiguration.isValid); - expect(tableValidator.isValid()).toBe(columnConfiguration.invalidKeys.length === 0); + expect(tableValidator.isValid()).toBe( + columnConfiguration.invalidKeys.length === 0 + ); expect(getInvalidKeys(tableValidator)).toEqual( jasmine.arrayWithExactContents( columnConfiguration.invalidKeys From dfc0cc0eb5b887725a310bde56a66646eebf193c Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:16:31 -0600 Subject: [PATCH 22/23] fix merge issues --- packages/nimble-components/src/table/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nimble-components/src/table/index.ts b/packages/nimble-components/src/table/index.ts index 1a4b16a3dd..bcd413d28c 100644 --- a/packages/nimble-components/src/table/index.ts +++ b/packages/nimble-components/src/table/index.ts @@ -130,7 +130,11 @@ export class Table< } } - private idFieldNameChanged( + protected childItemsChanged(): void { + void this.updateColumnsFromChildItems(); + } + + protected idFieldNameChanged( _prev: string | undefined, _next: string | undefined ): void { @@ -139,7 +143,7 @@ export class Table< this.setTableData(this.table.options.data); } - private columnsChanged( + protected columnsChanged( _prev: TableColumn[] | undefined, _next: TableColumn[] ): void { @@ -176,10 +180,6 @@ export class Table< this.canRenderRows = this.checkValidity(); } - protected childItemsChanged(): void { - void this.updateColumnsFromChildItems(); - } - private async updateColumnsFromChildItems(): Promise<void> { const definedElements = this.childItems.map(async item => (item.matches(':not(:defined)') ? customElements.whenDefined(item.localName) From 30a542335a3c295aaaa560b0d0153637a7a673a6 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:40:09 -0600 Subject: [PATCH 23/23] format --- packages/nimble-components/src/table/tests/table.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nimble-components/src/table/tests/table.spec.ts b/packages/nimble-components/src/table/tests/table.spec.ts index 24a1149142..2eda452999 100644 --- a/packages/nimble-components/src/table/tests/table.spec.ts +++ b/packages/nimble-components/src/table/tests/table.spec.ts @@ -114,7 +114,9 @@ describe('Table', () => { column1.textContent = 'stringData'; (column1 as TableColumnText).fieldName = 'stringData'; - const checkIcon = document.createElement(DesignSystem.tagFor(IconCheck)); + const checkIcon = document.createElement( + DesignSystem.tagFor(IconCheck) + ); column2 = document.createElement(tableColumnTextTag) as TableColumn; column2.appendChild(checkIcon); (column2 as TableColumnText).fieldName = 'moreStringData';