Skip to content

feat(cdk/table) scrollable table body #20414

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

Conversation

MichaelJamesParsons
Copy link
Collaborator

Changes

  • Added an internal API for overriding the DOM structure rendered by the CdkTable.
  • Added an experimental directive to enable scrollable body content for flex tables only.

Caveats

  • Sticky columns are not supported for tables with scrollable body content. This will be addressed in a follow-up pull request.

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Aug 25, 2020
Copy link
Contributor

@andrewseguin andrewseguin left a comment

Choose a reason for hiding this comment

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

Biggest thing for me is the API taking in the height - I would prefer that users don't need to pass this via their template, and it doesn't seem easy to have an auto type maxheight that matches the container

@@ -0,0 +1,10 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

I expect this can be deleted - these tag names won't show up in the example and they already apply flex styles

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

/** Adds native table sections (e.g. tbody) and moves the row outlets into them. */
private _applyNativeTableSections() {
private _applyNativeTableSections(): DocumentFragment {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to reflect that this won't apply anything, but instead create the standard document fragment for the native table sections, e.g. _getNativeTableSections()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

* Show a scroll bar if the table's body exceeds this height. The height may be specified with
* any valid CSS unit of measurement.
*/
@Input('scrollableBody')
Copy link
Contributor

Choose a reason for hiding this comment

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

I struggled a bit to get this case working: I want my table to expand in height to fit the content of the container, and I don't want to have to set a maxHeight. Is that possible to do with this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the desired behavior will vary for each app:

  1. Fixed height (e.g. 200px, 60vh, calc(100vh - OFFSET_FROM_TOP))
  2. Fill height of immediate parent element.
  3. Fill remaining space in a higher level parent element (e.g immediate parent is a card and the table should fill the remaining space on the page).

The first use case is covered. IMO, the other two should be supported in another directive.

Copy link
Member

Choose a reason for hiding this comment

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

What are the options for letting people set this via CSS? Turning it into a TS API makes it harder to e.g. use media queries, your Sass layout constants, etc.

export type _TableLayout = DocumentFragment | null;

/** Interface for a service that constructs the DOM structure for a {@link CdkTable}. */
export interface _TableLayoutStrategy {
Copy link
Contributor

Choose a reason for hiding this comment

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

I went back and forth on whether the API should just be getLayout, or what you have. I think what you have is fine - it's clear that there are two very separate layout types for the table and they should both be considered separately

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I also went back and forth on this and eventually chose this API for a few reasons:

  1. All implementations of a single getLayout() method would contain the same boilerplate code:
if (/* native table */) { /* native layout */ } else { /* flex layout */ }
  1. The explicit methods make the intent clearer and prevent erroneously returning the wrong layout for the table type.
  2. Some strategies may want to fall back on the default layout (e.g. scrollable body strategy is only supported for flex layouts, so the default layout is used for native tables).

}

if (!layout && this._isNativeHtmlTable) {
layout = this._applyNativeTableSections();
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems a little off to me that we use null as a way to signal to the table that it should fall back on a default. Should there not just be a default layout strategy that is always used, and that contains the native table document fragment builder?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sgtm. I extracted the default layout into its own _DefaultTableLayoutStrategy. LMK what you think.

@@ -6,6 +6,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
"menu",
"listbox",
"popover-edit",
"scrollable-table-body",
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure whether this should be in its own separate module since it's very closely coupled to the table. Maybe we can have material-experimental/table that exports a MatScrollableTableBody module? I believe that would allow build tools to remove the code if it isn't used, while grouping under table.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I'd also prefer this. Alternatively a tertiary entry-point under material(-experimental)/table/ could work too.


/** A directive that enables scrollable body content for flex tables. */
@Directive({
selector: 'cdk-table[scrollableBody], mat-table[scrollableBody]',
Copy link
Member

Choose a reason for hiding this comment

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

It seems weird to be mixing CDK and Material selectors within the same module. Generally we have the CDK as the base class and the Material variant extends it.

Copy link
Member

Choose a reason for hiding this comment

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

+1, we would need to separate these

protected readonly _viewRepeater: _ViewRepeater<T, RenderRow<T>, RowContext<T>>) {
protected readonly _viewRepeater: _ViewRepeater<T, RenderRow<T>, RowContext<T>>,
@Optional() @Inject(_TABLE_LAYOUT_STRATEGY)
private readonly _layoutStrategy: _TableLayoutStrategy|null) {
Copy link
Member

Choose a reason for hiding this comment

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

While the |null here is correct, I believe that ViewEngine will throw an error for it. You might have to make the parameter type non-null, declare a separate nullable property and assign to it:

private _layoutStrategy: _TableLayoutStrategy|null;

constructor(@Optional() @Inject(_TABLE_LAYOUT_STRATEGY) layoutStrategy: _TableLayoutStrategy) {
  this._layoutStrategy = layoutStrategy;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can we make this optional instead? Similar to this PR?

private readonly _layoutStrategy?: _TableLayoutStrategy

Copy link
Member

Choose a reason for hiding this comment

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

Yes, if I remember correctly optional parameters shouldn't throw.

/**
* Add basic flex styling so that the cells evenly space themselves in the row.
*/
cdk-row, cdk-header-row, cdk-footer-row {
Copy link
Member

Choose a reason for hiding this comment

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

It might be better to use the CSS classes for these selectors since they'll automatically work if we switch the example to use a table-based mat-table.

Copy link
Member

Choose a reason for hiding this comment

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

+1, we prefer using css classes for styling over element selectors, and in examples we always prefix styled with example- to make it clear which styles come from the component and which come from the example.

@@ -0,0 +1,33 @@
<button mat-button color="primary" (click)="addRow()">Add row</button>
<button mat-button color="primary" (click)="removeRow()">Remove row</button>
<button mat-button color="primary" (click)="nextMaxHeight()">Change max height</button>
Copy link
Member

Choose a reason for hiding this comment

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

nit: I don't think you're supposed to have multiple primary buttons next to each other.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, I did that out of habit. My team's UX mocks tend to have primary buttons next to each other.

I updated the template to align with the table demo (raised, default buttons).

return this.data;
}

disconnect() {}
Copy link
Member

Choose a reason for hiding this comment

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

Should the data stream be completed here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

@MichaelJamesParsons MichaelJamesParsons force-pushed the table-scrollable-body branch 2 times, most recently from 6200db9 to 92fc5d5 Compare August 27, 2020 17:46
new InjectionToken<_TableLayoutStrategy>('_TableLayoutStrategy');


export class _DefaultTableLayoutStrategy implements _TableLayoutStrategy {
Copy link
Member

Choose a reason for hiding this comment

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

I prefer to name classes for what they do, rather than how they're used (e.g. "Default" here). How about something like _StandardTableLayoutStrategy?

/** Interface for a service that constructs the DOM structure for a {@link CdkTable}. */
export interface _TableLayoutStrategy {
/** Constructs the DOM structure for a native table. */
getNativeLayout(table: CdkTable<any>): DocumentFragment;
Copy link
Member

Choose a reason for hiding this comment

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

I believe all of the any in this file should be unknown

(similar elsewhere where you don't know/care about the generic type)

readonly bodyCssClass = 'cdk-table-scrollable-table-body';
readonly footerCssClass = 'cdk-table-scrollable-table-footer';

constructor(@Inject(DOCUMENT) private readonly _document: any) {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than making the injected _document a class member with private, you can create a separate property with type Document so that the types are correct for uses throughout the file.

(we have to inject it as any otherwise SSR breaks due to how TypeScript decorators output)

Comment on lines 35 to 36
* Returns the DOM structure for a native table. Scrollable body content is not supported for
* native tables. Return `null` to use the default {@link CdkTable} native table layout.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe you discussed this with @andrewseguin and @mmalerba, but what's the background on this only supporting flexbox-based table? We want to strongly encourage people to use the native table, so not supporting that for virtual-scroll seems like a problem to me.


for (const section of sections) {
const element = this._document.createElement('div');
element.classList.add(section.selector);
Copy link
Member

Choose a reason for hiding this comment

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

selector should be cssClass since you're treating is as such here


/** A directive that enables scrollable body content for flex tables. */
@Directive({
selector: 'cdk-table[scrollableBody], mat-table[scrollableBody]',
Copy link
Member

Choose a reason for hiding this comment

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

+1, we would need to separate these

* Show a scroll bar if the table's body exceeds this height. The height may be specified with
* any valid CSS unit of measurement.
*/
@Input('scrollableBody')
Copy link
Member

Choose a reason for hiding this comment

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

What are the options for letting people set this via CSS? Turning it into a TS API makes it harder to e.g. use media queries, your Sass layout constants, etc.

/**
* Add basic flex styling so that the cells evenly space themselves in the row.
*/
cdk-row, cdk-header-row, cdk-footer-row {
Copy link
Member

Choose a reason for hiding this comment

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

+1, we prefer using css classes for styling over element selectors, and in examples we always prefix styled with example- to make it clear which styles come from the component and which come from the example.

@devversion devversion removed their request for review August 18, 2021 12:58
@andrewseguin andrewseguin removed the cla: yes PR author has agreed to Google's Contributor License Agreement label Dec 28, 2021
@andrewseguin
Copy link
Contributor

@MichaelJamesParsons Would you still like to try and land this change?

@andrewseguin andrewseguin self-assigned this Dec 11, 2024
@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 Jan 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants