-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
feat(cdk/table): virtual scroll directive for tables #21708
feat(cdk/table): virtual scroll directive for tables #21708
Conversation
...ts-examples/cdk-experimental/table/cdk-virtual-flex-table/cdk-virtual-flex-table-example.css
Outdated
Show resolved
Hide resolved
Since the description mentions an issue with flickering, could it be the same problem as #21576? |
Yes, though the flickering I'm seeing is not nearly as significant as observed in #21576. Likely due to internal performance optimizations that are present in this PR, but not yet accessible through the public API (e.g. recycle view repeater and enhancements made in #21704). |
src/cdk/table/table.ts
Outdated
// FIXME DO NOT SUBMIT: Is the following behavior acceptable? | ||
// | ||
// The table will override the StickyPositioningListener provider to `null` to prevent child | ||
// tables from inheriting it. However, if a directive on the table configures a new provider, it | ||
// should be used instead. Therefore, when a directive on the table configures a positioning | ||
// listener provider, the provider will be inherited by child tables. | ||
this._stickyPositioningListener = this._positioningListener ?? this._parentPositioningListener; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify that I understand this correctly: the goal here is to support multiple different directives adding their own sticky position listener? If that's the case, we should probably refactor this in one of two ways:
- Have one "notifier" that all those directives subscribe to
- Change this to a multi provider and let multiple directives register listeners
I would definitely lean towards the first option here as being more in line with the rest of the codebase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The table currently retrieves the StickyPositioningListener
provider from the parent component and overrides the provider to null
to prevent child tables from inheriting it.
<parent-with-listener>
<table></table>
</parent-with-listener>
However, the virtual scroll directive is bound directly on the table, so the table must get the provider from @Self()
when present.
<table virtualScroll></table>
2f7799d
to
2bf17c3
Compare
src/cdk/table/table.ts
Outdated
* Collection viewer for the `CdkTable`. For backwards compatibility, the collection viewer must | ||
* be a `BehaviorSubject`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can potentially change this; I assume that this isn't going to be widely used inside Google since viewChange
has just been a placeholder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Internal targets could be quickly migrated. What about external apps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a behavior change like this, I think this is something we could change for a major version (which we're currently in the window for) so long as we document it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jelbourn Following up since it's been a while. Is now a still a good time to make this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the benefit of changing it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The behavior subject emits a range the spans the entire data set when the table is initialized. This causes the table to briefly render all rows, then truncate them once it attaches to the virtual scroll viewport. By changing this to a regular Subject
, we'd have more control over when the range is emitted.
For now, I've hacked around it by overriding the behavior subject via provider. Then emitting an empty range as the initial value. This prevents rendering the rows before the virtual scroll viewport is attached.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd be able to change it starting in August
Revisiting this, could you expand the comment to touch on why this interface is necessary? (IIRC it's so that the table can inject the viewChange behavior from another directive instead of completely implementing it itself).
Actually typing that out, it might make sense to rename this to something named like ViewChangeXxx
instead of CollectionViewer
since CollectionViewer
could potentially be expanded to include more stuff, but that's not super important with the features it has today.
...ts-examples/cdk-experimental/table/cdk-virtual-flex-table/cdk-virtual-flex-table-example.css
Outdated
Show resolved
Hide resolved
55f1f96
to
e7b6450
Compare
04e4bcf
to
904f752
Compare
ad96443
to
99b0b31
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
src/cdk/table/table.ts
Outdated
* Collection viewer for the `CdkTable`. For backwards compatibility, the collection viewer must | ||
* be a `BehaviorSubject`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the benefit of changing it?
@jelbourn The
Local command:
Am I missing something? |
this._footerRowStickyUpdates.next(update); | ||
} | ||
|
||
private _stickHeaderRows(offsetFromTop: number, update: StickyUpdate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add some explanation here that touches on the need to apply these extra styles on top of/instead of what's happening with StickyStyler
? It might be because it's been long enough that I forgot how sticky positioning works today, but I feel like I'm missing context on why virtual scroll needs the extra styling logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a related note, #21576 (comment) mentioned that the flickering observed while scrolling quickly is potentially caused by the browser applying the viewport's transform
after top
. So the offsets occasionally get out of sync for 1 frame.
I took some time to experiment with this a bit more today and refactored the code to keep top
as-is, and use transformY
to shift the table cells into their correct positions. The end result was much worse than just updating top
directly.
Moving the headers/footers outside the scroll viewport entirely seems to be the only reliable solution to the flickering. In the future, it would be worth experimenting with an accessible flex table that scrolls only the body content.
src/cdk/table/table.scss
Outdated
@@ -1,3 +1,17 @@ | |||
.cdk-table-fixed-layout { | |||
table-layout: fixed; | |||
} | |||
|
|||
.cdk-table-virtual-scroll { | |||
border-collapse: collapse; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment for why border-collapse
is important here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tables with multiple headers/footers that have border-spacing
will initially render with spacing between cells. However, the spacing collapses after scrolling. I'm able to reproduce this with non-virtualized CDK tables, so I believe this is an issue with the stickystyler.
I added this style so border-spacing wouldn't collapse on scroll. It can probably be removed since it's an existing behavior with the sticky styler.
src/cdk/table/table.ts
Outdated
* Collection viewer for the `CdkTable`. For backwards compatibility, the collection viewer must | ||
* be a `BehaviorSubject`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd be able to change it starting in August
Revisiting this, could you expand the comment to touch on why this interface is necessary? (IIRC it's so that the table can inject the viewChange behavior from another directive instead of completely implementing it itself).
Actually typing that out, it might make sense to rename this to something named like ViewChangeXxx
instead of CollectionViewer
since CollectionViewer
could potentially be expanded to include more stuff, but that's not super important with the features it has today.
2382ec7
to
30d988c
Compare
I resolved the view engine failure described in #21708 (comment) by adding a missing build target to the @jelbourn Ready for another review. |
30d988c
to
5d9e66d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be wise to add an experimental-version table.md focused on documenting this feature to talk about usage, limitations, and perhaps performance considerations like making sure you use trackBy
. We'll want it for when it's moved out of experimental, and now would be a good time to write it while the details are fresh in your mind
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'}, | ||
]; | ||
|
||
const EXPANDED_ELEMENT_DATA: PeriodicElement[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comment to explain that this logic is simply to create a large dataset to so the table has a lot of rows
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
export class CdkVirtualTableExample { | ||
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol']; | ||
dataSource = EXPANDED_ELEMENT_DATA; | ||
trackBy = (index: number, el: PeriodicElement) => el.position; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is trackBy required for virtual scroll? Adding it to the example may imply that its necessary. If its for performance reasons, this would be a good spot to mention that whether it significantly improves things.
@@ -0,0 +1,37 @@ | |||
<cdk-virtual-scroll-viewport class="example-container" [itemSize]="48" [maxBufferPx]="1200" [minBufferPx]="400"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we provide a comment on why we chose these values for this example?
<th cdk-footer-cell *cdkFooterCellDef> Symbol </th> | ||
</ng-container> | ||
|
||
<tr cdk-header-row *cdkHeaderRowDef="displayedColumns; sticky: true;"></tr> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these extra rows are added to make sure we don't regress with the feature or performance, but it's not as useful to someone using this example to understand how to use the table.
Can we have a separate example that is more filled out or complex, and leave this as a barebones or typical example (e.g. single sticky header and no footer).
src/dev-app/main.ts
Outdated
@@ -11,5 +11,8 @@ import '@angular/localize/init'; | |||
|
|||
import {platformBrowser} from '@angular/platform-browser'; | |||
import {MainModuleNgFactory} from './main-module.ngfactory'; | |||
import {enableProdMode} from '@angular/core'; | |||
|
|||
enableProdMode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How come this is being added?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be reverted. Done.
@@ -37,8 +37,8 @@ import { | |||
export class _RecycleViewRepeaterStrategy<T, R, C extends _ViewRepeaterItemContext<T>> | |||
implements _ViewRepeater<T, R, C> { | |||
/** | |||
* The size of the cache used to store unused views. | |||
* Setting the cache size to `0` will disable caching. Defaults to 20 views. | |||
* The size of the cache used to store unused views. Setting the cache size to `0` will disable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be reverted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/** | ||
* Stream containing the latest information on what rows are being displayed on screen. | ||
* Can be used by the data source to as a heuristic of what data should be provided. | ||
* | ||
* @docs-private | ||
*/ | ||
readonly viewChange = | ||
new BehaviorSubject<{start: number, end: number}>({start: 0, end: Number.MAX_VALUE}); | ||
readonly viewChange: BehaviorSubject<ListRange>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make more sense to make this a ReplaySubject so that users will get the latest value on subscribe but you don't need to set an initial value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing this to a ReplaySubject would be a breaking change. Is it alright to make that change in this PR?
takeUntil(this._onDestroy)) | ||
.subscribe(([data, range]) => { | ||
this._data = data; | ||
this._renderedRange = range; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Should this be _renderRange
or _dataRange
to imply that this is the range we intend render, rather than the one we have rendered?
this._listener = listener; | ||
} | ||
|
||
stickyColumnsUpdated(update: StickyUpdate): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add documentation to each of these functions? The naming throws me off a little - they sound more like boolean values. Should they be something like onStickyColumnsUpdated
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These method names are inherited from the StickyPositioningListener
interface. I added inline docs to clarify their purpose.
* Measures the combined size (width for horizontal orientation, height for vertical) of all items | ||
* in the specified range. | ||
*/ | ||
measureRangeSize(range: ListRange, orientation: 'horizontal' | 'vertical'): number { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Can this be getRangeSize
instead of measure? Right now it sounds like its telling the directive to perform an action rather than to retrieve some values
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this method is must be implemented as part of the CdkVirtualScrollRepeater
interface. It's required to use the virtual scroll autosize
behavior.
@MichaelJamesParsons Would you like to rebase and continue iterating on this? |
Yes, I'll set aside time to wrap this up. |
5d9e66d
to
bb7fd59
Compare
bb7fd59
to
b29aac0
Compare
@andrewseguin would it be ok if I work on this one? I have the same issue here: #10122 (comment) and I see this hasn't been touched for sometime. Would love to help and have this one go over the finish line I think it makes sense for me to open up a new pull request all together given the time that has elapsed between now and then. Especially since signals now exists |
Closing since the code is fairly out of date and it's unlikely we'll get this refreshed. @CharlieGreenman you are welcome to open a new up-to-date PR for us to iterate on |
i will see what I can do |
Adds a
virtualRows
directive which enables virtual scrolling for CDK tables.Open questions
Blocks of code that are subject to further discussion or contain potentially breaking changes are annotated with
FIXME
.Out of scope for now
autosize
directive)Known issues
Performance considerations
We've already improved row rendering by 50% or better. Without headers/footers, the scrolling experience is comparable to most other Angular material virtual scroll demos.
However, the entire table shifts position as the user scrolls. The position of sticky headers and footers must be offset by the distance scrolled to keep them in place. On average, the table is able to keep 15-20ms per animation frame. Frames that occasionally exceed ~35ms cause sticky headers/footers to flicker. It looks like longer than usual style calculations are hindering performance the most.
While I believe we can further optimize rendering, we will likely reach a threshold where the table can no longer be optimized and non-trivial tables will still experience flickering (e.g. tables with inline editing, cells with custom components, etc.). A while back, I experimented with rendering tables in multiple pieces so the body content can scroll independently of header/footers (#20414). There are some a11y challenges to this solution, but it would resolve the flickering.
For example:
Related PRs