Skip to content

Commit 9aa4c30

Browse files
committed
feat(popover-edit): experimental popover edit for tables (mvp)
1 parent 876727d commit 9aa4c30

33 files changed

+1799
-6
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
/src/cdk-experimental/** @jelbourn
9090
/src/cdk-experimental/dialog/** @jelbourn @josephperrott @crisbeto
9191
/src/cdk-experimental/scrolling/** @mmalerba
92+
/src/cdk-experimental/popover-edit/** @kseamon @andrewseguin
9293

9394
# Docs examples & guides
9495
/guides/** @jelbourn
@@ -129,6 +130,7 @@
129130
/src/dev-app/paginator/** @andrewseguin
130131
/src/dev-app/platform/** @jelbourn @devversion
131132
/src/dev-app/portal/** @jelbourn
133+
/src/dev-app/popover-edit/** @kseamon @andrewseguin
132134
/src/dev-app/progress-bar/** @jelbourn @crisbeto @josephperrott
133135
/src/dev-app/progress-spinner/** @jelbourn @crisbeto @josephperrott
134136
/src/dev-app/radio/** @jelbourn @devversion

packages.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ CDK_TARGETS = ["//src/cdk"] + ["//src/cdk/%s" % p for p in CDK_PACKAGES]
2424

2525
CDK_EXPERIMENTAL_PACKAGES = [
2626
"dialog",
27+
"popover-edit",
2728
"scrolling",
2829
]
2930

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package(default_visibility=["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
4+
5+
ng_module(
6+
name = "popover-edit",
7+
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
8+
module_name = "@angular/cdk-experimental/popover-edit",
9+
deps = [
10+
"@npm//@angular/common",
11+
"@npm//@angular/core",
12+
"@npm//@angular/forms",
13+
"@npm//rxjs",
14+
"//src/cdk/a11y",
15+
"//src/cdk/overlay",
16+
"//src/cdk/portal",
17+
],
18+
)
19+
20+
ng_test_library(
21+
name = "popover_edit_test_sources",
22+
srcs = glob(["**/*.spec.ts"]),
23+
deps = [
24+
":popover-edit",
25+
"@npm//@angular/common",
26+
"@npm//@angular/forms",
27+
"@npm//rxjs",
28+
"//src/cdk/collections",
29+
"//src/cdk/table",
30+
],
31+
)
32+
33+
ng_web_test_suite(
34+
name = "unit_tests",
35+
deps = [":popover_edit_test_sources"]
36+
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ReplaySubject} from 'rxjs';
10+
import {OnDestroy} from '@angular/core';
11+
12+
export const DEFAULT_HOVER_DELAY_MS = 30;
13+
14+
export const CELL_SELECTOR = '.cdk-cell, .mat-cell, td';
15+
export const ROW_SELECTOR = '.cdk-row, .mat-row, tr';
16+
export const EDIT_PANE_CLASS = 'cdk-edit-pane';
17+
export const EDIT_PANE_SELECTOR = '.' + EDIT_PANE_CLASS;
18+
19+
/** IE 11 compatible matches implementation. */
20+
export function matches(element: Element, selector: string): boolean {
21+
return element.matches ?
22+
element.matches(selector) :
23+
(element as any)['msMatchesSelector'](selector);
24+
}
25+
26+
/** IE 11 compatible closest implementation. */
27+
export function closest(
28+
element: EventTarget|Element|null|undefined,
29+
selector: string) {
30+
if (!(element instanceof Node)) { return null; }
31+
32+
let curr: Node|null = element;
33+
while (curr != null && !(curr instanceof Element && matches(curr, selector))) {
34+
curr = curr.parentNode;
35+
}
36+
37+
return (curr || null) as Element|null;
38+
}
39+
40+
/** Convenience superclass for exposing a destroyed subject member. */
41+
export abstract class Destroyable implements OnDestroy {
42+
protected readonly destroyed = new ReplaySubject<void>();
43+
44+
ngOnDestroy() {
45+
this.destroyed.next();
46+
this.destroyed.complete();
47+
}
48+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Subject, timer} from 'rxjs';
10+
import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';
11+
import {Injectable} from '@angular/core';
12+
import {closest, CELL_SELECTOR, DEFAULT_HOVER_DELAY_MS, ROW_SELECTOR} from './common';
13+
14+
/**
15+
* Service for sharing delegated events and state for triggering edits.
16+
*/
17+
@Injectable()
18+
export class EditEvents {
19+
readonly editing = new Subject<Element|null>();
20+
readonly hovering = new Subject<Element|null>();
21+
readonly mouseMove = new Subject<Element|null>();
22+
23+
protected currentlyEditing: Element|null = null;
24+
25+
protected readonly hoveringDistinct = this.hovering.pipe(distinctUntilChanged(), share());
26+
protected readonly editingDistinct = this.editing.pipe(distinctUntilChanged(), share());
27+
28+
constructor() {
29+
this.editing.subscribe(cell => {
30+
this.currentlyEditing = cell;
31+
});
32+
}
33+
34+
/**
35+
* Returns an Observable that emits true when the specified element's cell
36+
* is editing and false when not.
37+
*/
38+
editingCell(element: Element|EventTarget) {
39+
let cell: Element|null = null;
40+
41+
return this.editing.pipe(
42+
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
43+
distinctUntilChanged(),
44+
);
45+
}
46+
47+
/**
48+
* Stops editing for the specified cell. If the specified cell is not the current
49+
* edit cell, does nothing.
50+
*/
51+
doneEditingCell(element: Element|EventTarget) {
52+
const cell = closest(element, CELL_SELECTOR);
53+
54+
if (this.currentlyEditing === cell) {
55+
this.editing.next(null);
56+
}
57+
}
58+
59+
/**
60+
* Returns an Observable that emits true when the specified element's row
61+
* is being hovered over and false when not. Hovering is defined as when
62+
* the mouse has momentarily stopped moving over the cell.
63+
*/
64+
hoveringOnRow(element: Element|EventTarget) {
65+
let row: Element|null = null;
66+
67+
return this.hoveringDistinct.pipe(
68+
map(hoveredRow => hoveredRow === (row || (row = closest(element, ROW_SELECTOR)))),
69+
audit((hovering) => hovering ?
70+
this.mouseMove.pipe(filter(hoveredRow => hoveredRow === row)) :
71+
timer(DEFAULT_HOVER_DELAY_MS)),
72+
distinctUntilChanged(),
73+
);
74+
}
75+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Subject} from 'rxjs';
10+
import {first} from 'rxjs/operators';
11+
import {Injectable, OnDestroy, Self} from '@angular/core';
12+
import {ControlContainer} from '@angular/forms';
13+
import {EditEvents} from './edit-events';
14+
15+
/**
16+
* Service used for communication between the form within the edit lens and the
17+
* table that launched it. Provided by CdkPopoverEditControl within the lens.
18+
*/
19+
@Injectable()
20+
export class EditRef implements OnDestroy {
21+
private readonly _finalValueSubject = new Subject<any>();
22+
readonly finalValue = this._finalValueSubject.asObservable();
23+
24+
private _revertFormValue: any;
25+
26+
constructor(
27+
@Self() private readonly _form: ControlContainer,
28+
private readonly _editEvents: EditEvents, ) {}
29+
30+
/**
31+
* Called by the host directive's OnInit hook. Reads the initial state of the
32+
* form and overrides it with persisted state from previous openings, if
33+
* applicable.
34+
*/
35+
init(previousFormValue: any) {
36+
// Wait for either the first value to be set, then override it with
37+
// the previously entered value, if any.
38+
this._form.valueChanges!.pipe(first()).subscribe(() => {
39+
this.updateRevertValue();
40+
41+
if (previousFormValue) {
42+
this.reset(previousFormValue);
43+
}
44+
});
45+
}
46+
47+
ngOnDestroy() {
48+
this._finalValueSubject.next(this._form.value);
49+
this._finalValueSubject.complete();
50+
}
51+
52+
/** Whether the attached form is in a valid state. */
53+
isValid() {
54+
return this._form.valid;
55+
}
56+
57+
/** Set the form's current value as what it will be set to on revert/reset. */
58+
updateRevertValue() {
59+
this._revertFormValue = this._form.value;
60+
}
61+
62+
/** Tells the table to close the edit popup. */
63+
close() {
64+
this._editEvents.editing.next(null);
65+
}
66+
67+
/**
68+
* Resets the form value to the specified value or the previously set
69+
* revert value.
70+
*/
71+
reset(value?: any) {
72+
this._form.reset(value || this._revertFormValue);
73+
}
74+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './public-api';

0 commit comments

Comments
 (0)