Skip to content

feat(table): enable multiple data rows #10964

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 2, 2018

Conversation

andrewseguin
Copy link
Contributor

@andrewseguin andrewseguin commented Apr 23, 2018

Introduces a new table input multiTemplateDataRows which allows the table to have multiple data rows per data object.

Old paradigm

Each data object passed to the table is rendered as a data row. Since the mapping is 1:1, the data array itself can be used with the IterableDiffer to determine which rows need to change.

New paradigm

Each data object passed to the table may be rendered by one or more rows. The IterableDiffer cannot use the data array itself anymore since it will not correctly understand add/remove/move operations in the rows.

Instead, the IterableDiffer will be diffed with a DataRow object that represents a pair containing a data object and row template. When the data changes, the list of DataRow objects will be constructed and diffed to get the list of changes.

Caveats for discussion

  1. The row context previously contained an index value that represented both the row index and data object index (they were the same. Now, that index value represents the index of the data. To get the row index, there is an additional context property called rowIndex.

Should it be swapped such that index is the row index and the data index can be stored in dataIndex?

  1. Similarly, the trackBy function receives an index value. Previously the index matched both the data and row. Now, its explicitly the data index.

Should trackBy receive the row index?

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Apr 23, 2018
@andrewseguin andrewseguin requested a review from jelbourn April 23, 2018 16:24
@andrewseguin andrewseguin force-pushed the table-eval-when-each-emit branch 2 times, most recently from f3870e0 to 52834b2 Compare April 23, 2018 22:53
@jelbourn
Copy link
Member

For the index stuff: I think that, when in multiple-row mode, you should have separate renderIndex and dataIndex properties for both the context and trackBy; I wouldn't make either just index because it will always be ambiguous. However, when in the standard mode, it should stay just index that that will by far be the most common case.

@@ -98,6 +99,16 @@ export const CDK_TABLE_TEMPLATE = `
*/
abstract class RowViewRef<T> extends EmbeddedViewRef<CdkCellOutletRowContext<T>> { }

/**
* Set of properties that represent a single rendered row. Acts as the row's identity for the
* `IterableDiffer`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand this to explain more of the why this is necessary? Should also explicitly say something like "There will be one DataRow instance per ____"

We also should avoid making this part of the public API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more to the comment, not sure whether its too verbose.

* `CdkRowDef<T>` object. With the two keys, the cache points to a `DataRow<T>` object that
* contains the pair.
*/
private _cachedDataRowsMap = new Map<T, Map<CdkRowDef<T>, DataRow<T>>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this use WeakMap? It doesn't look like you need to iterate over it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not. T may be a primitive value which would be an invalid WeakMap key.

* `CdkRowDef<T>` object. With the two keys, the cache points to a `DataRow<T>` object that
* contains the pair.
*/
private _cachedDataRowsMap = new Map<T, Map<CdkRowDef<T>, DataRow<T>>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this just be a map of T -> DataRow<T>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each T may map into multiple DataRow<T> objects since each object represents a pair of data and row template.

This setup allows us to uniquely identify these pairs so that they can be re-used if they show up again between renders. This is necessary for the IterableDiffer to understand the correct operations, e.g. when a pairing is moved.

* defined in the table, or otherwise the default row which does not have a when predicate.
*/
@Input()
get enableRowMultiplex(): boolean { return this._enableRowMultiplex; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so I refreshed my memory about multiplexers, and they actually do the opposite of what we want here: they take multiple input signals and output one. The opposite of that is "demuxing", and I think enableRowDemux is a bit too jargony for this API.

How about just multiTemplateRows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it should be multiTemplateDataRows since I intend to allow multiple header and footer rows which would not depend on this input.

@@ -262,8 +306,9 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._applyNativeTableSections();
}

// TODO(andrewseguin): Setup a listener for scrolling, emit the calculated view to viewChange
this._dataDiffer = this._differs.find([]).create(this._trackByFn);
this._dataDiffer = this._differs.find([]).create((_i: number, dataRow: DataRow<T>) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment explaining what is happening here? It's not quite clear to me

* row definitions. If the previous list already contained a particular pair, it should be reused
* so that the differ equates their references.
*/
private _getDataRows(): DataRow<T>[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having both _getDataRows and _getDataRowsForData is confusing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, struggled coming up with a better name for these. One returns the list of all the DataRow objects to render, while one returns the list of DataRow objects for a particular data object.

Any ideas?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think _getAllRenderRows and _getRenderRowsForData would be good

@andrewseguin
Copy link
Contributor Author

Making the index and naming changes; will add pr: needs review label when I pushed the latest

@andrewseguin
Copy link
Contributor Author

Ready for review

/** Index location of the rendered row that this cell is located within. */
renderIndex?: number;

/** Index of the data object in the provided data array. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove index?

dataSource: FakeDataSource = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
isIndex1Columns = ['index1Column'];
hasC3Columns = ['c3Column'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These names are pretty cryptic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to columnsForIsIndex1Row and columnsForHasC3Row. Not totally sure thats better :)

*/
export interface RenderRow<T> {
data: T;
index: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataIndex or renderIndex?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to dataIndex

get multiTemplateRows(): boolean { return this._multiTemplateRows; }
set multiTemplateRows(v: boolean) {
this._multiTemplateRows = coerceBooleanProperty(v);
if (!!this._rowOutlet.viewContainer.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!! is unnecessary in conditionals

// respective `RenderRow` object which is the pair of `data` and `CdkRowDef`.
for (let i = 0; i < this._data.length; i++) {
let data = this._data[i];
const dataRowsForData = this._getRenderRowsForData(data, i, prevCachedRenderRows.get(data));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderRowsForData

private _insertRow(rowData: T, index: number) {
const rowDef = this._getRowDef(rowData, index);
const context: CdkCellOutletRowContext<T> = {$implicit: rowData};
private _insertRow(dataRow: RenderRow<T>, index: number) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderRow

odd?: boolean;
}

/** Context provided to the row cells when `multiTemplateDataRows` is true */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand to say something like

This is the same as CdkCellOutletRowContext, except that the single `index` is
replaced with `renderIndex` and `dataIndex`.

* @docs-private
*/
export interface RowContext<T>
extends CdkCellOutletMultiRowContext<T>, CdkCellOutletRowContext<T> { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't want to extend both, you want a union type

export type RowContext = CdkCellOutletMultiRowContext | CdkCellOutletRowContext;

Copy link
Contributor Author

@@ -620,4 +755,11 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._elementRef.nativeElement.appendChild(element);
}
}

/** Forces a re-render of the data rows. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand this comment with information about when it's appropriate to call this function?

@@ -56,6 +56,23 @@ describe('MatTable', () => {
['Footer A', 'Footer B', 'Footer C'],
]);
});

it('should create a table with row multiplex', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multiplex

@andrewseguin
Copy link
Contributor Author

Thanks, should have done a better job of renaming dataRow to renderRow. Forgot to find/replace that lowercase one version

@andrewseguin andrewseguin added the merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note label Apr 26, 2018
@andrewseguin
Copy link
Contributor Author

Note to caretaker, will require BUILD change to add ":cdk_coercion" to the cdk_table BUILD rule.

See change 194450307 for patch.


// For each data object, get the list of rows that should be rendered, represented by the
// respective `RenderRow` object which is the pair of `data` and `CdkRowDef`.
for (let i = 0; i < this._data.length; i++) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check/handle if empty

@andrewseguin
Copy link
Contributor Author

Removing pr: needs review label. Found that having homogeneous data was not correctly being handled between diffs due to caching a RenderRow for each data object rather than per rendered row.

Writing up some tests and will add the label and a comment when ready again.

@andrewseguin andrewseguin force-pushed the table-eval-when-each-emit branch 2 times, most recently from d64a8ff to 75ced9a Compare April 30, 2018 17:37
@andrewseguin andrewseguin added target: minor This PR is targeted for the next minor release pr: needs review labels Apr 30, 2018
@andrewseguin andrewseguin force-pushed the table-eval-when-each-emit branch from 75ced9a to fe56e56 Compare April 30, 2018 17:41
@andrewseguin
Copy link
Contributor Author

Ready again for review. Added docs on the table's implementation process for rendering rows. Renamed the input to multiTemplateDataRows since the input will only affect data rows

}

/**
* Returns a list of `RenderRow<T>` for the provided data object and any `CdkRowDef` objects that
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer "Gets" to "Returns` in descriptions

* predicate that returns true with the data. If none return true, return the default row
* definition.
*/
_getRowDef(data: T, i: number): CdkRowDef<T> {
if (this._rowDefs.length == 1) { return this._rowDefs[0]; }
_getRowDefs(data: T, i: number): CdkRowDef<T>[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still make this more specific than i?

]);
});

it('should have the correct data and row indicies when data is homogeneous', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Homogeneous" doesn't really communicate the same thing as "Data contains multiple occurrences of the same object instance"

@andrewseguin andrewseguin force-pushed the table-eval-when-each-emit branch from 373bfa9 to 075d680 Compare April 30, 2018 22:35
@andrewseguin
Copy link
Contributor Author

Thanks - made the changes

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jelbourn jelbourn added pr: lgtm action: merge The PR is ready for merge by the caretaker and removed pr: needs review labels May 1, 2018
@josephperrott josephperrott requested review from josephperrott and removed request for josephperrott May 1, 2018 22:34
@josephperrott josephperrott merged commit 0e466ee into angular:master May 2, 2018
josephperrott added a commit that referenced this pull request May 2, 2018
crisbeto pushed a commit to crisbeto/material2 that referenced this pull request May 2, 2018
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 8, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants