Skip to content
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(column-resize): Experimental column resize for mat-table #16114

Merged
merged 8 commits into from
Feb 13, 2020

Conversation

kseamon
Copy link
Collaborator

@kseamon kseamon commented May 24, 2019

Plus some incomplete cdk parts.

To come in follow-up PRs:

  • Support for interoperating with mat-sort-header
  • Support for persisting and restoring column sizes
  • Keyboard/accessibility support
  • Support for touch interfaces

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label May 24, 2019
@kseamon
Copy link
Collaborator Author

kseamon commented May 24, 2019

#8312

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.

I got about halfway through before I got too sleepy and decided to go home

protected readonly destroyed = new ReplaySubject<void>();
protected readonly document: Document;

get minPx(): number {
Copy link
Member

Choose a reason for hiding this comment

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

How impractical would it be to support more units for minx/max?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It would ultimately require the ability to convert them into pixels for use while resizing is in progress. I'm not going to do it, but someone probably could.

@kseamon kseamon force-pushed the column-resize-almost-mvp branch from 543b8fd to 25043c6 Compare May 31, 2019 18:15
@kseamon
Copy link
Collaborator Author

kseamon commented May 31, 2019

Addressed existing comments and made some progress on getting tests set up. Still not ready to leave draft state yet.

@kseamon kseamon force-pushed the column-resize-almost-mvp branch from 25043c6 to 753fc20 Compare June 10, 2019 13:46
@kseamon kseamon force-pushed the column-resize-almost-mvp branch 3 times, most recently from e2ca82c to 472b594 Compare July 1, 2019 18:59
@kseamon kseamon marked this pull request as ready for review July 1, 2019 18:59
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.

Is it the intent that some additional padding is necessary in the headers when adding the resize handle?
image

* The optimally performing resize strategy for flex mat-tables.
* Tested against and outperformed:
* CSS selector w/ CSS variable
* Updating all mat-cell nodes
Copy link
Member

Choose a reason for hiding this comment

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

Were these tests something persistent that we could reference in the future, or just something local/transient?

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 just ran them locally in the development app with 100 and 500 rows.

I could imagine a unit test that recreates that scenario, but I'd rather not be the one to write it :P

@jelbourn jelbourn requested review from andrewseguin and crisbeto and removed request for devversion and mmalerba July 2, 2019 21:24
@jelbourn
Copy link
Member

jelbourn commented Jul 2, 2019

@crisbeto @andrewseguin PTAL if you have time

@narendrasinghrathore
Copy link

narendrasinghrathore commented Jul 15, 2019

@jelbourn Is it patched in upcoming updates for mat-table ?
Waiting for this feature badly for mat-table column drag-drop, resize and multiple headers.
Regards

TABLE_LAYOUT_FIXED_RESIZE_STRATEGY_PROVIDER,
];
export const FLEX_PROVIDERS: Provider[] = [...PROVIDERS, FLEX_RESIZE_STRATEGY_PROVIDER];
export const HOST_BINDINGS = {
Copy link
Member

Choose a reason for hiding this comment

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

It feels like overkill to have to maintain this in a separate constant.

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'll punt this one up to @jelbourn for adjudication - I like the DRYness of it.

this.ngZone.runOutsideAngular(() => {
const element = this.elementRef.nativeElement!;

fromEvent<MouseEvent>(element, 'mouseover').pipe(
Copy link
Member

Choose a reason for hiding this comment

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

Don't we want mouseenter 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.

mouseenter isn't very useful for delegated events. This is a single listener for the whole table.

takeUntil(this.destroyed),
map(event => closest(event.target, HEADER_CELL_SELECTOR)),
).subscribe(this.eventDispatcher.headerCellHovered);
fromEvent<MouseEvent>(element, 'mouseleave').pipe(
Copy link
Member

Choose a reason for hiding this comment

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

How are we going to handle this for touch devices?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is a great question that I need to think more about.

My leading idea at the moment is to show the resize handles when there is focus within the header row (hopefully the user tapping there will trigger that), or failing that, listen for taps within the header row and show them until a tap outside of the row.

Added touch as a follow-up PR todo in the PR description.


private _dragStarted(mousedownEvent: MouseEvent) {
const mouseup = fromEvent<MouseEvent>(this.document, 'mouseup');
const mousemove = fromEvent<MouseEvent>(this.document, 'mousemove');
Copy link
Member

Choose a reason for hiding this comment

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

Should we handle touch events here? Usually it's much easier to do it up-front than adding it later on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes - I need to do this, but ideally in a follow up PR as this one is quite big already (will add to my to do list in the PR description).

// If the mouse moved further than the resize was able to match, limit the
// movement of the overlay to match the actual size and position of the origin.
if (overshot !== 0) {
if (overshot < 0 && deltaX < 0 || overshot > 0 && deltaX > 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Could we reuse the drag-drop module from the CDK here? I know that @andrewseguin managed to implement a resizeable sidenav with it which is very similar to what we need to do for the table.

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 looked at it when I was designing this and it seemed more straightforward to do it this way. It's been a while, so I don't remember my exact thought process though.

Copy link
Member

Choose a reason for hiding this comment

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

Worthwhile to add a TODO for now, we can revisit when we want to move out of experimental

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO added at top of file

overlayY: 'top',
}]);

return this.overlay.create({
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 I follow why we need an overlay here. Is it just so we can move the handle while dragging?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In my design doc for this (Google-internal, sorry), I cover this. Basically, there's no better way to get the styling right for the drag handle as it has to visually bleed into both the next and previous cells and also go down the entire table while the dragging is happening according to the internal UI spec I'm basing this on (which is an extension of the public Material Design spec that includes things like Datatable resizing and inline edit)

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 encode that context into a comment in this file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added

applyMinColumnSize(cssFriendlyColumnName: string, _: HTMLElement, sizeInPx: number): void {
const cssSize = coerceCssPixelValue(sizeInPx);

this._applyProperty(cssFriendlyColumnName, 'min-width', cssSize,
Copy link
Member

Choose a reason for hiding this comment

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

Have you tried this against IE/Edge? They tend to not work very well with min-width/min-height and flexbox.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Honestly I'm not even sure how I'd test that (I only have Macs and Linux boxes here at work). Is this still a concern with Chromium-based Edge?

Copy link
Member

Choose a reason for hiding this comment

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

Probably not, but the Chromium-based Edge is still away from being the default.

const selector = `.${tableClassName} .${columnClassName}`;
const body = propertyKeys.map(key => `${key}:${properties.get(key)}`).join(';');

this._getStyleSheet().insertRule(`${selector} {${body}}`, index!);
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be simpler to assign the styles directly on the element, rather than having to keep track of a separate stylesheet?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It would be simpler, but I'm doing this as an optimization to avoid doing a dom manipulation for every row in the table.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There'd also be the issue of dealing with rows added later on, which this handles nicely.

@andrewseguin
Copy link
Contributor

Change looks good, and the demo works great. Users are going to be excited about this one

My only comment on the PR is that it looks like a lot of the functions and properties are missing JSDocs. I know it's quite a bit to document, but do you mind sweeping through with comments for them?

@kseamon
Copy link
Collaborator Author

kseamon commented Jan 3, 2020

Working on reviving this at long last. Going to respond to some of the above comments then take on the actionable ones after I get all of this rebased to the modern world.

@kseamon
Copy link
Collaborator Author

kseamon commented Jan 3, 2020

@jelbourn Re: the padding question

Yes - I think you'd want to have padding inside your cells when using the resize feature. The internal spec has 8px I think.

@kseamon kseamon force-pushed the column-resize-almost-mvp branch from 472b594 to e97a7bd Compare January 3, 2020 21:03
@kseamon kseamon requested a review from a team as a code owner January 3, 2020 21:03
@kseamon kseamon force-pushed the column-resize-almost-mvp branch from e97a7bd to 0aa2d47 Compare January 3, 2020 21:57
Copy link
Collaborator Author

@kseamon kseamon left a comment

Choose a reason for hiding this comment

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

Added a bunch of JSDoc. Please take a look

@kseamon kseamon force-pushed the column-resize-almost-mvp branch from 53e2210 to 2c620fb Compare January 27, 2020 16:34
@jelbourn jelbourn added the target: minor This PR is targeted for the next minor release label Jan 28, 2020
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.

Took a fresh look at this- just a handful more things I noticed.

Would also be good for @crisbeto and @andrewseguin to take a fresh look

readonly completeImmediately?: boolean;
}

/** Conduit for resize-releated events within the table. */
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/** Conduit for resize-releated events within the table. */
/** Originating source of column resize events within a table. */

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

overlayY: 'top',
}]);

return this.overlay.create({
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 encode that context into a comment in this file?

this.resizeNotifier.resizeCanceled,
this.resizeNotifier.resizeCompleted,
).pipe(takeUntilDestroyed).subscribe(columnSize => {
this.elementRef.nativeElement!.classList.remove(OVERLAY_ACTIVE_CLASS);
Copy link
Member

Choose a reason for hiding this comment

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

This method is called _listenForResizeEvents, but this doesn't really describe what happens on resize. Break this part into one or more methods that have a descriptive name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Re: "Can you encode that context into a comment in this file?", I'm not sure I understand what you're asking me to do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(nevermind - this was super confusing in the conversation view but I see what you were talking about now)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Split two methods out of _listenForResizeEvents

}
}

const OVERLAY_ACTIVE_CLASS = 'cdk-resizable-overlay-thumb-active';
Copy link
Member

Choose a reason for hiding this comment

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

Move this to the top of the file?

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

@@ -0,0 +1,16 @@
/**
Copy link
Member

Choose a reason for hiding this comment

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

How about calling this file selectors.ts?

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

// If the mouse moved further than the resize was able to match, limit the
// movement of the overlay to match the actual size and position of the origin.
if (overshot !== 0) {
if (overshot < 0 && deltaX < 0 || overshot > 0 && deltaX > 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Worthwhile to add a TODO for now, we can revisit when we want to move out of experimental

Copy link
Member

@crisbeto crisbeto left a comment

Choose a reason for hiding this comment

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

LGTM once all the comments are addressed.

* Base class for ColumnResize directives which attach to mat-table elements to
* provide common events and services for column resizing.
*/
export abstract class ColumnResize implements AfterViewInit, OnDestroy {
Copy link
Member

Choose a reason for hiding this comment

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

This should have a blank @Directive() decorator since it's using Angular features.

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

abstract readonly columnResizeNotifier: ColumnResizeNotifier;

abstract readonly directionality: Directionality;
protected abstract readonly elementRef: ElementRef;
Copy link
Member

Choose a reason for hiding this comment

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

Should be ElementRef<HTMLElement> for stronger typing.

* Base class for a component shown over the edge of a resizable column that is responsible
* for handling column resize mouse events and displaying any visible UI on the column edge.
*/
export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
Copy link
Member

Choose a reason for hiding this comment

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

Should be marked as @Directive.

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

* for handling column resize mouse events and displaying any visible UI on the column edge.
*/
export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
protected readonly destroyed = new ReplaySubject<void>();
Copy link
Member

Choose a reason for hiding this comment

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

I don't see why this needs to be a ReplaySubject. Since it only ever emits once and completes, it can be a regular Subject.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's a safety precaution that I've adopted as my default practice - if something subscribes after this is destroyed (perhaps due to some async event or timer) then ReplaySubject will cause it to immediately unsubscribe whereas a normal subject never would.

* provide common events and services for column resizing.
*/
export abstract class ColumnResize implements AfterViewInit, OnDestroy {
protected readonly destroyed = new ReplaySubject<void>();
Copy link
Member

Choose a reason for hiding this comment

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

This should also be a regular Subject.


fromEvent<MouseEvent>(this.elementRef.nativeElement!, 'mouseleave').pipe(
takeUntilDestroyed,
map(event => event.relatedTarget &&
Copy link
Member

Choose a reason for hiding this comment

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

Have you tested this out in different browsers? I know that IE at least used to have some issues with relatedTarget.

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 don't have a good way to test it on IE (I just have access to Mac, Linux, ChomeOS and iOS). I'd appreciate help sorting things like this out before this graduates from experimental.

Copy link
Member

Choose a reason for hiding this comment

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

Windows, IE11, and Edge (non-Chromium) run fine for me on my MBPs via VirtualBox, VMWare Fusion, and Bootcamp. VirtualBox and Bootcamp are free.

applyMinColumnSize(cssFriendlyColumnName: string, _: HTMLElement, sizeInPx: number): void {
const cssSize = coerceCssPixelValue(sizeInPx);

this._applyProperty(cssFriendlyColumnName, 'min-width', cssSize,
Copy link
Member

Choose a reason for hiding this comment

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

Probably not, but the Chromium-based Edge is still away from being the default.

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.

LGTM - though in general I would prefer to see more jsdocs for class methods

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 labels Feb 6, 2020
@kseamon kseamon force-pushed the column-resize-almost-mvp branch from 6d44a55 to 1d71ac2 Compare February 13, 2020 16:09
@jelbourn jelbourn merged commit 198911f into angular:master Feb 13, 2020
@olegkuibar
Copy link

@kseamon is there any estimate release date for this ? Thx.

@kseamon
Copy link
Collaborator Author

kseamon commented Mar 5, 2020 via email

@jelbourn
Copy link
Member

jelbourn commented Mar 5, 2020

The current code is usable from @angular/cdk-experimental, but there's no ETA yet on when this will hit feature completion and stable

@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 Apr 5, 2020
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 target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants