mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(popover-edit): experimental popover edit for tables (mvp) (#15496)
Adds a new set of directives to cdk-experimental that allow editing of content in tables (including cdk-table). Will be expanded with additional functionality and Material-styled versions in coming PRs.
This commit is contained in:
@@ -89,6 +89,7 @@
|
||||
/src/cdk-experimental/** @jelbourn
|
||||
/src/cdk-experimental/dialog/** @jelbourn @josephperrott @crisbeto
|
||||
/src/cdk-experimental/scrolling/** @mmalerba
|
||||
/src/cdk-experimental/popover-edit/** @kseamon @andrewseguin
|
||||
|
||||
# Docs examples & guides
|
||||
/guides/** @jelbourn
|
||||
@@ -130,6 +131,7 @@
|
||||
/src/dev-app/paginator/** @andrewseguin
|
||||
/src/dev-app/platform/** @jelbourn @devversion
|
||||
/src/dev-app/portal/** @jelbourn
|
||||
/src/dev-app/popover-edit/** @kseamon @andrewseguin
|
||||
/src/dev-app/progress-bar/** @jelbourn @crisbeto @josephperrott
|
||||
/src/dev-app/progress-spinner/** @jelbourn @crisbeto @josephperrott
|
||||
/src/dev-app/radio/** @jelbourn @devversion
|
||||
|
||||
@@ -24,6 +24,7 @@ CDK_TARGETS = ["//src/cdk"] + ["//src/cdk/%s" % p for p in CDK_PACKAGES]
|
||||
|
||||
CDK_EXPERIMENTAL_PACKAGES = [
|
||||
"dialog",
|
||||
"popover-edit",
|
||||
"scrolling",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package(default_visibility=["//visibility:public"])
|
||||
|
||||
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
|
||||
|
||||
ng_module(
|
||||
name = "popover-edit",
|
||||
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
|
||||
module_name = "@angular/cdk-experimental/popover-edit",
|
||||
deps = [
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/core",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//rxjs",
|
||||
"//src/cdk/a11y",
|
||||
"//src/cdk/overlay",
|
||||
"//src/cdk/portal",
|
||||
],
|
||||
)
|
||||
|
||||
ng_test_library(
|
||||
name = "popover_edit_test_sources",
|
||||
srcs = glob(["**/*.spec.ts"]),
|
||||
deps = [
|
||||
":popover-edit",
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//rxjs",
|
||||
"//src/cdk/collections",
|
||||
"//src/cdk/table",
|
||||
],
|
||||
)
|
||||
|
||||
ng_web_test_suite(
|
||||
name = "unit_tests",
|
||||
deps = [":popover_edit_test_sources"]
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/** Selector for finding table cells. */
|
||||
export const CELL_SELECTOR = '.cdk-cell, .mat-cell, td';
|
||||
|
||||
/** Selector for finding table rows. */
|
||||
export const ROW_SELECTOR = '.cdk-row, .mat-row, tr';
|
||||
|
||||
/** CSS class added to the edit lens pane. */
|
||||
export const EDIT_PANE_CLASS = 'cdk-edit-pane';
|
||||
|
||||
/** Selector for finding the edit lens pane. */
|
||||
export const EDIT_PANE_SELECTOR = '.' + EDIT_PANE_CLASS;
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable, Subject, timer} from 'rxjs';
|
||||
import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';
|
||||
|
||||
import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
|
||||
import {closest} from './polyfill';
|
||||
|
||||
/** The delay between mouse out events and hiding hover content. */
|
||||
const DEFAULT_MOUSE_OUT_DELAY_MS = 30;
|
||||
|
||||
/**
|
||||
* Service for sharing delegated events and state for triggering table edits.
|
||||
*/
|
||||
@Injectable()
|
||||
export class EditEventDispatcher {
|
||||
/** A subject that indicates which table cell is currently editing. */
|
||||
readonly editing = new Subject<Element|null>();
|
||||
|
||||
/** A subject that indicates which table row is currently hovered. */
|
||||
readonly hovering = new Subject<Element|null>();
|
||||
|
||||
/** A subject that emits mouse move events for table rows. */
|
||||
readonly mouseMove = new Subject<Element|null>();
|
||||
|
||||
/** The table cell that has an active edit lens (or null). */
|
||||
private _currentlyEditing: Element|null = null;
|
||||
|
||||
private readonly _hoveringDistinct = this.hovering.pipe(distinctUntilChanged(), share());
|
||||
private readonly _editingDistinct = this.editing.pipe(distinctUntilChanged(), share());
|
||||
|
||||
constructor() {
|
||||
this._editingDistinct.subscribe(cell => {
|
||||
this._currentlyEditing = cell;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an Observable that emits true when the specified element's cell
|
||||
* is editing and false when not.
|
||||
*/
|
||||
editingCell(element: Element|EventTarget): Observable<boolean> {
|
||||
let cell: Element|null = null;
|
||||
|
||||
return this._editingDistinct.pipe(
|
||||
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops editing for the specified cell. If the specified cell is not the current
|
||||
* edit cell, does nothing.
|
||||
*/
|
||||
doneEditingCell(element: Element|EventTarget): void {
|
||||
const cell = closest(element, CELL_SELECTOR);
|
||||
|
||||
if (this._currentlyEditing === cell) {
|
||||
this.editing.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an Observable that emits true when the specified element's row
|
||||
* is being hovered over and false when not. Hovering is defined as when
|
||||
* the mouse has momentarily stopped moving over the cell.
|
||||
*/
|
||||
hoveringOnRow(element: Element|EventTarget): Observable<boolean> {
|
||||
let row: Element|null = null;
|
||||
|
||||
return this._hoveringDistinct.pipe(
|
||||
map(hoveredRow => hoveredRow === (row || (row = closest(element, ROW_SELECTOR)))),
|
||||
audit(
|
||||
(hovering) => hovering ? this.mouseMove.pipe(filter(hoveredRow => hoveredRow === row)) :
|
||||
timer(DEFAULT_MOUSE_OUT_DELAY_MS)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable, OnDestroy, Self} from '@angular/core';
|
||||
import {ControlContainer} from '@angular/forms';
|
||||
import {Subject} from 'rxjs';
|
||||
import {take} from 'rxjs/operators';
|
||||
|
||||
import {EditEventDispatcher} from './edit-event-dispatcher';
|
||||
|
||||
/**
|
||||
* Used for communication between the form within the edit lens and the
|
||||
* table that launched it. Provided by CdkEditControl within the lens.
|
||||
*/
|
||||
@Injectable()
|
||||
export class EditRef<FormValue> implements OnDestroy {
|
||||
/** Emits the final value of this edit instance before closing. */
|
||||
private readonly _finalValueSubject = new Subject<FormValue>();
|
||||
readonly finalValue = this._finalValueSubject.asObservable();
|
||||
|
||||
/** The value to set the form back to on revert. */
|
||||
private _revertFormValue: FormValue;
|
||||
|
||||
/**
|
||||
* The flags are used to track whether a keyboard enter press is in progress at the same time
|
||||
* as other events that would cause the edit lens to close. We must track this so that the
|
||||
* Enter keyup event does not fire after we close as it would cause the edit to immediately
|
||||
* reopen.
|
||||
*/
|
||||
private _enterPressed = false;
|
||||
private _closePending = false;
|
||||
|
||||
constructor(
|
||||
@Self() private readonly _form: ControlContainer,
|
||||
private readonly _editEventDispatcher: EditEventDispatcher) {}
|
||||
|
||||
/**
|
||||
* Called by the host directive's OnInit hook. Reads the initial state of the
|
||||
* form and overrides it with persisted state from previous openings, if
|
||||
* applicable.
|
||||
*/
|
||||
init(previousFormValue: FormValue|undefined): void {
|
||||
// Wait for either the first value to be set, then override it with
|
||||
// the previously entered value, if any.
|
||||
this._form.valueChanges!.pipe(take(1)).subscribe(() => {
|
||||
this.updateRevertValue();
|
||||
|
||||
if (previousFormValue) {
|
||||
this.reset(previousFormValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._finalValueSubject.next(this._form.value);
|
||||
this._finalValueSubject.complete();
|
||||
}
|
||||
|
||||
/** Whether the attached form is in a valid state. */
|
||||
isValid(): boolean|null {
|
||||
return this._form.valid;
|
||||
}
|
||||
|
||||
/** Set the form's current value as what it will be set to on revert/reset. */
|
||||
updateRevertValue(): void {
|
||||
this._revertFormValue = this._form.value;
|
||||
}
|
||||
|
||||
/** Tells the table to close the edit popup. */
|
||||
close(): void {
|
||||
this._editEventDispatcher.editing.next(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the edit if the enter key is not down.
|
||||
* Otherwise, sets _closePending to true so that the edit will close on the
|
||||
* next enter keyup.
|
||||
*/
|
||||
closeAfterEnterKeypress(): void {
|
||||
// If the enter key is currently pressed, delay closing the popup so that
|
||||
// the keyUp event does not cause it to immediately reopen.
|
||||
if (this._enterPressed) {
|
||||
this._closePending = true;
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on Enter keyup/keydown.
|
||||
* Closes the edit if pending. Otherwise just updates _enterPressed.
|
||||
*/
|
||||
trackEnterPressForClose(pressed: boolean): void {
|
||||
if (this._closePending) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this._enterPressed = pressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the form value to the specified value or the previously set
|
||||
* revert value.
|
||||
*/
|
||||
reset(value?: FormValue): void {
|
||||
this._form.reset(value || this._revertFormValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export * from './public-api';
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ReplaySubject} from 'rxjs';
|
||||
import {Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
|
||||
import {EDIT_PANE_SELECTOR} from './constants';
|
||||
import {closest} from './polyfill';
|
||||
import {EditRef} from './edit-ref';
|
||||
|
||||
/** Options for what do to when the user clicks outside of an edit lens. */
|
||||
export type PopoverEditClickOutBehavior = 'close' | 'submit' | 'noop';
|
||||
|
||||
/**
|
||||
* A directive that attaches to a form within the edit lens.
|
||||
* It coordinates the form state with the table-wide edit system and handles
|
||||
* closing the edit lens when the form is submitted or the user clicks
|
||||
* out.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'form[cdkEditControl]',
|
||||
host: {
|
||||
'(ngSubmit)': 'handleFormSubmit()',
|
||||
'(keydown.enter)': 'editRef.trackEnterPressForClose(true)',
|
||||
'(keyup.enter)': 'editRef.trackEnterPressForClose(false)',
|
||||
'(keyup.escape)': 'close()',
|
||||
'(document:click)': 'handlePossibleClickOut($event)',
|
||||
},
|
||||
providers: [EditRef],
|
||||
})
|
||||
export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
|
||||
protected readonly destroyed = new ReplaySubject<void>();
|
||||
|
||||
/**
|
||||
* Specifies what should happen when the user clicks outside of the edit lens.
|
||||
* The default behavior is to close the lens without submitting the form.
|
||||
*/
|
||||
@Input('cdkEditControlClickOutBehavior') clickOutBehavior: PopoverEditClickOutBehavior = 'close';
|
||||
|
||||
/**
|
||||
* A two-way binding for storing unsubmitted form state. If not provided
|
||||
* then form state will be discarded on close. The PeristBy directive is offered
|
||||
* as a convenient shortcut for these bindings.
|
||||
*/
|
||||
@Input('cdkEditControlPreservedFormValue') preservedFormValue?: FormValue;
|
||||
@Output('cdkEditControlPreservedFormValueChange') readonly preservedFormValueChange =
|
||||
new EventEmitter<FormValue>();
|
||||
|
||||
/**
|
||||
* Determines whether the lens will close on form submit if the form is not in a valid
|
||||
* state. By default the lens will remain open.
|
||||
*/
|
||||
@Input('cdkEditControlIgnoreSubmitUnlessValid') ignoreSubmitUnlessValid = true;
|
||||
|
||||
constructor(protected readonly elementRef: ElementRef, readonly editRef: EditRef<FormValue>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.editRef.init(this.preservedFormValue);
|
||||
this.editRef.finalValue.subscribe(this.preservedFormValueChange);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the form submits. If ignoreSubmitUnlessValid is true, checks
|
||||
* the form for validity before proceeding.
|
||||
* Updates the revert state with the latest submitted value then closes the edit.
|
||||
*/
|
||||
handleFormSubmit(): void {
|
||||
if (this.ignoreSubmitUnlessValid && !this.editRef.isValid()) { return; }
|
||||
|
||||
this.editRef.updateRevertValue();
|
||||
this.editRef.closeAfterEnterKeypress();
|
||||
}
|
||||
|
||||
/** Called on Escape keyup. Closes the edit. */
|
||||
close(): void {
|
||||
// todo - allow this behavior to be customized as well, such as calling
|
||||
// reset before close
|
||||
this.editRef.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on click anywhere in the document.
|
||||
* If the click was outside of the lens, trigger the specified click out behavior.
|
||||
*/
|
||||
handlePossibleClickOut(evt: Event): void {
|
||||
if (closest(evt.target, EDIT_PANE_SELECTOR)) { return; }
|
||||
|
||||
switch (this.clickOutBehavior) {
|
||||
case 'submit':
|
||||
// Manually cause the form to submit before closing.
|
||||
this.elementRef.nativeElement!.dispatchEvent(new Event('submit'));
|
||||
// Fall through
|
||||
case 'close':
|
||||
this.editRef.close();
|
||||
// Fall through
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Reverts the form to its initial or previously submitted state on click. */
|
||||
@Directive({
|
||||
selector: 'button[cdkEditRevert]',
|
||||
host: {
|
||||
'(click)': 'revertEdit()',
|
||||
'type': 'button', // Prevents accidental form submits.
|
||||
}
|
||||
})
|
||||
export class CdkEditRevert<FormValue> {
|
||||
constructor(
|
||||
protected readonly editRef: EditRef<FormValue>) {}
|
||||
|
||||
revertEdit(): void {
|
||||
this.editRef.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the lens on click. */
|
||||
@Directive({
|
||||
selector: 'button[cdkEditClose]',
|
||||
host: {
|
||||
'(click)': 'closeEdit()',
|
||||
'type': 'button', // Prevents accidental form submits.
|
||||
}
|
||||
})
|
||||
export class CdkEditClose<FormValue> {
|
||||
constructor(
|
||||
protected readonly editRef: EditRef<FormValue>) {}
|
||||
|
||||
closeEdit(): void {
|
||||
this.editRef.closeAfterEnterKeypress();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/** IE 11 compatible matches implementation. */
|
||||
export function matches(element: Element, selector: string): boolean {
|
||||
return element.matches ?
|
||||
element.matches(selector) :
|
||||
(element as any)['msMatchesSelector'](selector);
|
||||
}
|
||||
|
||||
/** IE 11 compatible closest implementation. */
|
||||
export function closest(element: EventTarget|Element|null|undefined, selector: string): Element|
|
||||
null {
|
||||
if (!(element instanceof Node)) { return null; }
|
||||
|
||||
let curr: Node|null = element;
|
||||
while (curr != null && !(curr instanceof Element && matches(curr, selector))) {
|
||||
curr = curr.parentNode;
|
||||
}
|
||||
|
||||
return (curr || null) as Element|null;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
CdkPopoverEdit,
|
||||
CdkRowHoverContent,
|
||||
CdkEditable,
|
||||
CdkEditOpen,
|
||||
CdkEditOpenButton
|
||||
} from './table-directives';
|
||||
import {CdkEditControl,
|
||||
CdkEditRevert,
|
||||
CdkEditClose
|
||||
} from './lens-directives';
|
||||
import {
|
||||
DefaultPopoverEditPositionStrategyFactory,
|
||||
PopoverEditPositionStrategyFactory
|
||||
} from './popover-edit-position-strategy-factory';
|
||||
|
||||
const EXPORTED_DECLARATIONS = [
|
||||
CdkPopoverEdit,
|
||||
CdkRowHoverContent,
|
||||
CdkEditControl,
|
||||
CdkEditRevert,
|
||||
CdkEditClose,
|
||||
CdkEditable,
|
||||
CdkEditOpen,
|
||||
CdkEditOpenButton,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
OverlayModule,
|
||||
],
|
||||
exports: EXPORTED_DECLARATIONS,
|
||||
declarations: EXPORTED_DECLARATIONS,
|
||||
providers: [{
|
||||
provide: PopoverEditPositionStrategyFactory,
|
||||
useClass: DefaultPopoverEditPositionStrategyFactory
|
||||
}],
|
||||
})
|
||||
export class CdkPopoverEditModule { }
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ElementRef, Injectable} from '@angular/core';
|
||||
import {Overlay, PositionStrategy} from '@angular/cdk/overlay';
|
||||
|
||||
/**
|
||||
* Overridable factory responsible for configuring how cdkPopoverEdit popovers are positioned.
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class PopoverEditPositionStrategyFactory {
|
||||
abstract forElementRef(elementRef: ElementRef): PositionStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of PopoverEditPositionStrategyFactory.
|
||||
* Uses a FlexibleConnectedPositionStrategy anchored to the start + top of the cell.
|
||||
* Note: This will change to CoverPositionStrategy once it implemented.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DefaultPopoverEditPositionStrategyFactory extends PopoverEditPositionStrategyFactory {
|
||||
constructor(protected readonly overlay: Overlay) {
|
||||
super();
|
||||
}
|
||||
|
||||
forElementRef(elementRef: ElementRef): PositionStrategy {
|
||||
return this.overlay.position().flexibleConnectedTo(elementRef)
|
||||
.withGrowAfterOpen()
|
||||
.withPush()
|
||||
.withPositions([{
|
||||
originX: 'start',
|
||||
originY: 'top',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
}]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {Component, ElementRef, Type, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule, NgForm} from '@angular/forms';
|
||||
import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
|
||||
import {DataSource} from '@angular/cdk/collections';
|
||||
import {CdkTableModule} from '@angular/cdk/table';
|
||||
import {CdkPopoverEditModule, PopoverEditClickOutBehavior} from './index';
|
||||
|
||||
const EDIT_TEMPLATE = `
|
||||
<div style="background-color: white;">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
(ngSubmit)="onSubmit(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedValues.set(element, $event)"
|
||||
[cdkEditControlIgnoreSubmitUnlessValid]="ignoreSubmitUnlessValid"
|
||||
[cdkEditControlClickOutBehavior]="clickOutBehavior">
|
||||
<input [ngModel]="element.name" name="name" required>
|
||||
<br>
|
||||
<button class="submit" type="submit">Confirm</button>
|
||||
<button class="revert" cdkEditRevert>Revert</button>
|
||||
<button class="close" cdkEditClose>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const CELL_TEMPLATE = `
|
||||
{{element.name}}
|
||||
|
||||
<span *cdkRowHoverContent>
|
||||
<button class="open" cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
`;
|
||||
|
||||
interface PeriodicElement {
|
||||
name: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
abstract class BaseTestComponent {
|
||||
@ViewChild('table') table: ElementRef;
|
||||
|
||||
preservedValues = new Map<number, PeriodicElement>();
|
||||
|
||||
ignoreSubmitUnlessValid = true;
|
||||
clickOutBehavior: PopoverEditClickOutBehavior = 'close';
|
||||
|
||||
onSubmit(element: PeriodicElement, form: NgForm) {
|
||||
if (!form.valid) { return; }
|
||||
|
||||
element.name = form.value['name'];
|
||||
}
|
||||
|
||||
triggerHoverState(rowIndex = 0) {
|
||||
const row = getRows(this.table.nativeElement)[rowIndex];
|
||||
row.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
tick(31);
|
||||
}
|
||||
|
||||
getEditCell(rowIndex = 0) {
|
||||
const row = getRows(this.table.nativeElement)[rowIndex];
|
||||
return getCells(row)[0];
|
||||
}
|
||||
|
||||
focusEditCell(rowIndex = 0) {
|
||||
this.getEditCell(rowIndex).focus();
|
||||
}
|
||||
|
||||
getOpenButton(rowIndex = 0) {
|
||||
return this.getEditCell(rowIndex).querySelector('.open') as HTMLElement|null;
|
||||
}
|
||||
|
||||
clickOpenButton(rowIndex = 0) {
|
||||
this.getOpenButton(rowIndex)!.click();
|
||||
}
|
||||
|
||||
openLens(rowIndex = 0) {
|
||||
this.focusEditCell(rowIndex);
|
||||
this.getEditCell(rowIndex).dispatchEvent(
|
||||
new KeyboardEvent('keyup', {bubbles: true, key: 'Enter'}));
|
||||
flush();
|
||||
}
|
||||
|
||||
getInput() {
|
||||
return document.querySelector('input') as HTMLInputElement|null;
|
||||
}
|
||||
|
||||
lensIsOpen() {
|
||||
return !!this.getInput();
|
||||
}
|
||||
|
||||
getSubmitButton() {
|
||||
return document.querySelector('.submit') as HTMLElement|null;
|
||||
}
|
||||
|
||||
clickSubmitButton() {
|
||||
this.getSubmitButton()!.click();
|
||||
}
|
||||
|
||||
getRevertButton() {
|
||||
return document.querySelector('.revert') as HTMLElement|null;
|
||||
}
|
||||
|
||||
clickRevertButton() {
|
||||
this.getRevertButton()!.click();
|
||||
}
|
||||
|
||||
getCloseButton() {
|
||||
return document.querySelector('.close') as HTMLElement|null;
|
||||
}
|
||||
|
||||
clickCloseButton() {
|
||||
this.getCloseButton()!.click();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<table #table editable>
|
||||
<ng-template #nameEdit let-element>
|
||||
${EDIT_TEMPLATE}
|
||||
</ng-template>
|
||||
|
||||
<tr *ngFor="let element of elements">
|
||||
<td [cdkPopoverEdit]="nameEdit" [cdkPopoverEditContext]="element">
|
||||
${CELL_TEMPLATE}
|
||||
</td>
|
||||
|
||||
<td> {{element.weight}} </td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
})
|
||||
class VanillaTableOutOfCell extends BaseTestComponent {
|
||||
elements = createElementData();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<table #table editable>
|
||||
<tr *ngFor="let element of elements">
|
||||
<td [cdkPopoverEdit]="nameEdit">
|
||||
${CELL_TEMPLATE}
|
||||
|
||||
<ng-template #nameEdit>
|
||||
${EDIT_TEMPLATE}
|
||||
</ng-template>
|
||||
</td>
|
||||
|
||||
<td> {{element.weight}} </td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
})
|
||||
class VanillaTableInCell extends BaseTestComponent {
|
||||
elements = createElementData();
|
||||
}
|
||||
|
||||
class ElementDataSource extends DataSource<PeriodicElement> {
|
||||
/** Stream of data that is provided to the table. */
|
||||
data = new BehaviorSubject(createElementData());
|
||||
|
||||
/** Connect function called by the table to retrieve one stream containing the data to render. */
|
||||
connect() {
|
||||
return this.data.asObservable();
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div #table>
|
||||
<cdk-table cdk-table editable [dataSource]="dataSource">
|
||||
<ng-container cdkColumnDef="name">
|
||||
<cdk-cell *cdkCellDef="let element"
|
||||
[cdkPopoverEdit]="nameEdit">
|
||||
${CELL_TEMPLATE}
|
||||
|
||||
<ng-template #nameEdit>
|
||||
${EDIT_TEMPLATE}
|
||||
</ng-template>
|
||||
|
||||
<span *cdkIfRowHovered>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</cdk-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container cdkColumnDef="weight">
|
||||
<cdk-cell *cdkCellDef="let element">
|
||||
{{element.weight}}
|
||||
</cdk-cell>
|
||||
</ng-container>
|
||||
|
||||
<cdk-row *cdkRowDef="let row; columns: displayedColumns;"></cdk-row>
|
||||
</cdk-table>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
class CdkFlexTableInCell extends BaseTestComponent {
|
||||
displayedColumns = ['name', 'weight'];
|
||||
dataSource = new ElementDataSource();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div #table>
|
||||
<table cdk-table editable [dataSource]="dataSource">
|
||||
<ng-container cdkColumnDef="name">
|
||||
<td cdk-cell *cdkCellDef="let element"
|
||||
[cdkPopoverEdit]="nameEdit">
|
||||
${CELL_TEMPLATE}
|
||||
|
||||
<ng-template #nameEdit>
|
||||
${EDIT_TEMPLATE}
|
||||
</ng-template>
|
||||
|
||||
<span *cdkIfRowHovered>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container cdkColumnDef="weight">
|
||||
<td cdk-cell *cdkCellDef="let element">
|
||||
{{element.weight}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr cdk-row *cdkRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<div>
|
||||
`
|
||||
})
|
||||
class CdkTableInCell extends BaseTestComponent {
|
||||
displayedColumns = ['name', 'weight'];
|
||||
dataSource = new ElementDataSource();
|
||||
}
|
||||
|
||||
const testCases: ReadonlyArray<[Type<BaseTestComponent>, string]> = [
|
||||
[VanillaTableOutOfCell, 'Vanilla HTML table; edit defined outside of cell'],
|
||||
[VanillaTableInCell, 'Vanilla HTML table; edit defined within cell'],
|
||||
[CdkFlexTableInCell, 'Flex cdk-table; edit defined within cell'],
|
||||
[CdkTableInCell, 'Table cdk-table; edit defined within cell'],
|
||||
];
|
||||
|
||||
describe('CDK Popover Edit', () => {
|
||||
for (const [componentClass, label] of testCases) {
|
||||
describe(label, () => {
|
||||
let component: BaseTestComponent;
|
||||
let fixture: ComponentFixture<BaseTestComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CdkTableModule, CdkPopoverEditModule, CommonModule, FormsModule],
|
||||
declarations: [componentClass],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(componentClass);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('triggering edit', () => {
|
||||
it('shows and hides on-hover content only after a delay', fakeAsync(() => {
|
||||
const [row0, row1] = getRows(component.table.nativeElement);
|
||||
row0.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
|
||||
tick(20);
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
tick(20);
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
|
||||
tick(11);
|
||||
|
||||
expect(component.getOpenButton(0)).toEqual(jasmine.any(HTMLElement));
|
||||
|
||||
row1.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row1.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
expect(component.getOpenButton(0)).toEqual(jasmine.any(HTMLElement));
|
||||
expect(component.getOpenButton(1)).toBe(null);
|
||||
|
||||
tick(31);
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
expect(component.getOpenButton(1)).toEqual(jasmine.any(HTMLElement));
|
||||
}));
|
||||
|
||||
it('opens edit from on-hover button', fakeAsync(() => {
|
||||
component.triggerHoverState();
|
||||
component.clickOpenButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
}));
|
||||
|
||||
it('opens edit from Enter on focued cell', fakeAsync(() => {
|
||||
// Uses Enter to open the lens.
|
||||
component.openLens();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('edit lens', () => {
|
||||
it('shows a lens with the value from the table', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydrogen');
|
||||
}));
|
||||
|
||||
it('updates the form and submits, closing the lens', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
|
||||
component.clickSubmitButton();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydragon');
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
}));
|
||||
|
||||
it('does not close the lens on submit when form is invalid', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = '';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
|
||||
component.clickSubmitButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
}));
|
||||
|
||||
it('closes lens on submit when form is invalid with ' +
|
||||
'cdkEditControlIgnoreSubmitUnlessValid = false', fakeAsync(() => {
|
||||
component.ignoreSubmitUnlessValid = false;
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = '';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
|
||||
component.clickSubmitButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
}));
|
||||
|
||||
it('closes the lens on close', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.clickCloseButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
}));
|
||||
|
||||
it('closes and reopens a lens with modified value persisted', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
|
||||
component.clickCloseButton();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
|
||||
component.openLens();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydragon');
|
||||
}));
|
||||
|
||||
it('resets the lens to original value', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
|
||||
component.clickRevertButton();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydrogen');
|
||||
}));
|
||||
|
||||
it('resets the lens to previously submitted value', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
|
||||
component.clickSubmitButton();
|
||||
fixture.detectChanges();
|
||||
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon X';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
|
||||
component.clickRevertButton();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydragon');
|
||||
}));
|
||||
|
||||
it('closes the lens on escape', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.dispatchEvent(
|
||||
new KeyboardEvent('keyup', {bubbles: true, key: 'Escape'}));
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
}));
|
||||
|
||||
it('does not close the lens on click within lens', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
}));
|
||||
|
||||
it('closes the lens on outside click', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
document.body.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
|
||||
}));
|
||||
|
||||
it('submits the lens on outside click with ' +
|
||||
'cdkEditControlClickOutBehavior = "submit"', fakeAsync(() => {
|
||||
component.clickOutBehavior = 'submit';
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
document.body.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydragon');
|
||||
}));
|
||||
|
||||
it('does nothing on outside click with ' +
|
||||
'cdkEditControlClickOutBehavior = "noop"', fakeAsync(() => {
|
||||
component.clickOutBehavior = 'noop';
|
||||
component.openLens();
|
||||
|
||||
component.getInput()!.value = 'Hydragon';
|
||||
component.getInput()!.dispatchEvent(new Event('input'));
|
||||
document.body.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
}));
|
||||
|
||||
it('sets focus on the first input in the lens', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
expect(document.activeElement).toBe(component.getInput());
|
||||
}));
|
||||
|
||||
it('returns focus to the edited cell after closing', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
component.clickCloseButton();
|
||||
|
||||
expect(document.activeElement).toBe(component.getEditCell());
|
||||
}));
|
||||
|
||||
it('does not focus to the edited cell after closing if another element ' +
|
||||
'outside the lens is already focused', fakeAsync(() => {
|
||||
component.openLens(0);
|
||||
|
||||
component.getEditCell(1).focus();
|
||||
component.getEditCell(1).dispatchEvent(new Event('click', {bubbles: true}));
|
||||
|
||||
expect(document.activeElement).toBe(component.getEditCell(1));
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function createElementData() {
|
||||
return [
|
||||
{name: 'Hydrogen', weight: 1.007},
|
||||
{name: 'Helium', weight: 4.0026},
|
||||
{name: 'Lithium', weight: 6.941},
|
||||
{name: 'Beryllium', weight: 9.0122},
|
||||
{name: 'Boron', weight: 10.81},
|
||||
];
|
||||
}
|
||||
|
||||
function getElements(element: Element, query: string): HTMLElement[] {
|
||||
return [].slice.call(element.querySelectorAll(query));
|
||||
}
|
||||
|
||||
function getRows(tableElement: Element): HTMLElement[] {
|
||||
return getElements(tableElement, '.cdk-row, tr');
|
||||
}
|
||||
|
||||
function getCells(row: Element): HTMLElement[] {
|
||||
if (!row) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getElements(row, '.cdk-cell, td');
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export * from './edit-event-dispatcher';
|
||||
export * from './edit-ref';
|
||||
export * from './lens-directives';
|
||||
export * from './popover-edit-module';
|
||||
export * from './popover-edit-position-strategy-factory';
|
||||
export * from './table-directives';
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
|
||||
import {Overlay, OverlayRef} from '@angular/cdk/overlay';
|
||||
import {TemplatePortal} from '@angular/cdk/portal';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EmbeddedViewRef,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
TemplateRef,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import {fromEvent, ReplaySubject} from 'rxjs';
|
||||
import {debounceTime, filter, map, mapTo, takeUntil} from 'rxjs/operators';
|
||||
|
||||
import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants';
|
||||
import {EditEventDispatcher} from './edit-event-dispatcher';
|
||||
import {closest} from './polyfill';
|
||||
import {PopoverEditPositionStrategyFactory} from './popover-edit-position-strategy-factory';
|
||||
|
||||
/**
|
||||
* The delay between the mouse entering a row and the mouse stopping its movement before
|
||||
* showing on-hover content.
|
||||
*/
|
||||
const DEFAULT_MOUSE_MOVE_DELAY_MS = 30;
|
||||
|
||||
/**
|
||||
* A directive that must be attached to enable editability on a table.
|
||||
* It is responsible for setting up delegated event handlers and providing the
|
||||
* EditEventDispatcher service for use by the other edit directives.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'table[editable], cdk-table[editable], mat-table[editable]',
|
||||
providers: [EditEventDispatcher],
|
||||
})
|
||||
export class CdkEditable implements AfterViewInit, OnDestroy {
|
||||
protected readonly destroyed = new ReplaySubject<void>();
|
||||
|
||||
constructor(
|
||||
protected readonly elementRef: ElementRef,
|
||||
protected readonly editEventDispatcher: EditEventDispatcher,
|
||||
protected readonly ngZone: NgZone) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._listenForTableEvents();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
}
|
||||
|
||||
private _listenForTableEvents(): void {
|
||||
const element = this.elementRef.nativeElement!;
|
||||
|
||||
const toClosest = (selector: string) =>
|
||||
map((event: UIEvent) => closest(event.target, selector));
|
||||
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
fromEvent<MouseEvent>(element, 'mouseover').pipe(
|
||||
takeUntil(this.destroyed),
|
||||
toClosest(ROW_SELECTOR),
|
||||
).subscribe(this.editEventDispatcher.hovering);
|
||||
fromEvent<MouseEvent>(element, 'mouseleave').pipe(
|
||||
takeUntil(this.destroyed),
|
||||
mapTo(null),
|
||||
).subscribe(this.editEventDispatcher.hovering);
|
||||
fromEvent<MouseEvent>(element, 'mousemove').pipe(
|
||||
takeUntil(this.destroyed),
|
||||
debounceTime(DEFAULT_MOUSE_MOVE_DELAY_MS),
|
||||
toClosest(ROW_SELECTOR),
|
||||
).subscribe(this.editEventDispatcher.mouseMove);
|
||||
|
||||
fromEvent<KeyboardEvent>(element, 'keyup').pipe(
|
||||
takeUntil(this.destroyed),
|
||||
filter(event => event.key === 'Enter'),
|
||||
toClosest(CELL_SELECTOR),
|
||||
).subscribe(this.editEventDispatcher.editing);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches an ng-template to a cell and shows it when instructed to by the
|
||||
* EditEventDispatcher service.
|
||||
* Makes the cell focusable.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[cdkPopoverEdit]',
|
||||
host: {
|
||||
'tabIndex': '0',
|
||||
'[attr.aria-haspopup]': 'true',
|
||||
}
|
||||
})
|
||||
export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
|
||||
/** The edit lens template shown over the cell on edit. */
|
||||
@Input('cdkPopoverEdit') template: TemplateRef<any>|null = null;
|
||||
|
||||
/**
|
||||
* Implicit context to pass along to the template. Can be omitted if the template
|
||||
* is defined within the cell.
|
||||
*/
|
||||
@Input('cdkPopoverEditContext') context?: C;
|
||||
|
||||
protected focusTrap?: FocusTrap;
|
||||
protected overlayRef?: OverlayRef;
|
||||
protected readonly destroyed = new ReplaySubject<void>();
|
||||
|
||||
constructor(
|
||||
protected readonly editEventDispatcher: EditEventDispatcher,
|
||||
protected readonly elementRef: ElementRef,
|
||||
protected readonly focusTrapFactory: FocusTrapFactory,
|
||||
protected readonly ngZone: NgZone,
|
||||
protected readonly overlay: Overlay,
|
||||
protected readonly positionFactory: PopoverEditPositionStrategyFactory,
|
||||
protected readonly viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._startListeningToEditEvents();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
if (this.overlayRef) {
|
||||
this.overlayRef.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private _startListeningToEditEvents(): void {
|
||||
this.editEventDispatcher.editingCell(this.elementRef.nativeElement!)
|
||||
.pipe(takeUntil(this.destroyed))
|
||||
.subscribe((open) => {
|
||||
this.ngZone.run(() => {
|
||||
if (open && this.template) {
|
||||
if (!this.overlayRef) {
|
||||
this._createEditOverlay();
|
||||
}
|
||||
|
||||
this._showEditOverlay();
|
||||
} else if (this.overlayRef) {
|
||||
this._maybeReturnFocusToCell();
|
||||
|
||||
this.overlayRef.detach();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _createEditOverlay(): void {
|
||||
this.overlayRef = this.overlay.create({
|
||||
disposeOnNavigation: true,
|
||||
panelClass: EDIT_PANE_CLASS,
|
||||
positionStrategy: this.positionFactory.forElementRef(this.elementRef),
|
||||
scrollStrategy: this.overlay.scrollStrategies.reposition({autoClose: true}),
|
||||
});
|
||||
|
||||
this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement);
|
||||
this.overlayRef.overlayElement.setAttribute('aria-role', 'dialog');
|
||||
|
||||
this.overlayRef.detachments().subscribe(() => {
|
||||
this.editEventDispatcher.doneEditingCell(this.elementRef.nativeElement!);
|
||||
});
|
||||
}
|
||||
|
||||
private _showEditOverlay(): void {
|
||||
this.overlayRef!.attach(new TemplatePortal(
|
||||
this.template!,
|
||||
this.viewContainerRef,
|
||||
{$implicit: this.context}));
|
||||
this.focusTrap!.focusInitialElementWhenReady();
|
||||
}
|
||||
|
||||
private _maybeReturnFocusToCell(): void {
|
||||
if (closest(document.activeElement, EDIT_PANE_SELECTOR) ===
|
||||
this.overlayRef!.overlayElement) {
|
||||
this.elementRef.nativeElement!.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structural directive that shows its contents when the table row containing
|
||||
* it is hovered.
|
||||
*
|
||||
* Note that the contents of this directive are invisible to screen readers.
|
||||
* Typically this is used to show a button that launches the edit popup, which
|
||||
* is ok because screen reader users can trigger edit by pressing Enter on a focused
|
||||
* table cell.
|
||||
*
|
||||
* If this directive contains buttons for functionality other than opening edit then
|
||||
* care should be taken to make sure that this functionality is also exposed in
|
||||
* an accessible way.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[cdkRowHoverContent]',
|
||||
host: {
|
||||
'[attr.aria-hidden]': 'true',
|
||||
}
|
||||
})
|
||||
export class CdkRowHoverContent implements AfterViewInit, OnDestroy {
|
||||
protected readonly destroyed = new ReplaySubject<void>();
|
||||
protected viewRef: EmbeddedViewRef<any>|null = null;
|
||||
|
||||
constructor(
|
||||
protected readonly elementRef: ElementRef,
|
||||
protected readonly editEventDispatcher: EditEventDispatcher,
|
||||
protected readonly ngZone: NgZone,
|
||||
protected readonly templateRef: TemplateRef<any>,
|
||||
protected readonly viewContainerRef: ViewContainerRef
|
||||
) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._listenForHoverEvents();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
if (this.viewRef) {
|
||||
this.viewRef.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private _listenForHoverEvents(): void {
|
||||
this.editEventDispatcher.hoveringOnRow(this.elementRef.nativeElement!)
|
||||
.pipe(takeUntil(this.destroyed))
|
||||
.subscribe(isHovering => {
|
||||
this.ngZone.run(() => {
|
||||
if (isHovering) {
|
||||
if (!this.viewRef) {
|
||||
// Not doing any positioning in CDK version. Material version
|
||||
// will absolutely position on right edge of cell.
|
||||
this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef, {});
|
||||
} else {
|
||||
this.viewContainerRef.insert(this.viewRef);
|
||||
}
|
||||
} else if (this.viewRef) {
|
||||
this.viewContainerRef.detach(this.viewContainerRef.indexOf(this.viewRef));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the closest edit popover to this element, whether it's associated with this exact
|
||||
* element or an ancestor element.
|
||||
*/
|
||||
@Directive({
|
||||
// Specify :not(button) as we only need to add type: button on actual buttons.
|
||||
selector: '[cdkEditOpen]:not(button)',
|
||||
host: {
|
||||
'(click)': 'openEdit($event)',
|
||||
}
|
||||
})
|
||||
export class CdkEditOpen {
|
||||
constructor(
|
||||
protected readonly elementRef: ElementRef,
|
||||
protected readonly editEventDispatcher: EditEventDispatcher) {}
|
||||
|
||||
openEdit(evt: Event): void {
|
||||
this.editEventDispatcher.editing.next(closest(this.elementRef.nativeElement!, CELL_SELECTOR));
|
||||
evt.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the closest edit popover to this element, whether it's associated with this exact
|
||||
* element or an ancestor element.
|
||||
*/
|
||||
@Directive({
|
||||
// Specify button as we only need to add type: button on actual buttons.
|
||||
selector: 'button[cdkEditOpen]',
|
||||
host: {
|
||||
'(click)': 'openEdit($event)',
|
||||
'type': 'button', // Prevents accidental form submits.
|
||||
}
|
||||
})
|
||||
export class CdkEditOpenButton extends CdkEditOpen {}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../tsconfig-build",
|
||||
"files": [
|
||||
"public-api.ts",
|
||||
"../typings.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true,
|
||||
"flatModuleOutFile": "index.js",
|
||||
"flatModuleId": "@angular/cdk-experimental/popover-edit",
|
||||
"skipTemplateCodegen": true,
|
||||
"fullTemplateTypeCheck": true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package(default_visibility=["//visibility:public"])
|
||||
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
load("//:packages.bzl", "MATERIAL_TARGETS", "CDK_TARGETS")
|
||||
load("//:packages.bzl", "MATERIAL_TARGETS", "CDK_TARGETS", "CDK_EXPERIMENTAL_TARGETS")
|
||||
load("//tools:defaults.bzl", "ng_module")
|
||||
load("//tools:sass_generate_binaries.bzl", "sass_generate_binaries")
|
||||
|
||||
@@ -25,9 +25,8 @@ ng_module(
|
||||
"@npm//@angular/platform-browser-dynamic",
|
||||
"@npm//@angular/router",
|
||||
"@npm//rxjs",
|
||||
"//src/cdk-experimental",
|
||||
"//src/material-examples:examples",
|
||||
] + CDK_TARGETS + MATERIAL_TARGETS
|
||||
] + CDK_TARGETS + CDK_EXPERIMENTAL_TARGETS + MATERIAL_TARGETS
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
|
||||
@@ -49,6 +49,7 @@ export class DevAppLayout {
|
||||
{name: 'Menu', route: '/menu'},
|
||||
{name: 'Paginator', route: '/paginator'},
|
||||
{name: 'Platform', route: '/platform'},
|
||||
{name: 'Popover Edit', route: '/popover-edit'},
|
||||
{name: 'Portal', route: '/portal'},
|
||||
{name: 'Progress Bar', route: '/progress-bar'},
|
||||
{name: 'Progress Spinner', route: '/progress-spinner'},
|
||||
|
||||
@@ -51,6 +51,10 @@ export const DEV_APP_ROUTES: Routes = [
|
||||
{path: 'menu', loadChildren: 'menu/menu-demo-module#MenuDemoModule'},
|
||||
{path: 'paginator', loadChildren: 'paginator/paginator-demo-module#PaginatorDemoModule'},
|
||||
{path: 'platform', loadChildren: 'platform/platform-demo-module#PlatformDemoModule'},
|
||||
{
|
||||
path: 'popover-edit',
|
||||
loadChildren: 'popover-edit/popover-edit-demo-module#PopoverEditDemoModule'
|
||||
},
|
||||
{path: 'portal', loadChildren: 'portal/portal-demo-module#PortalDemoModule'},
|
||||
{
|
||||
path: 'progress-bar',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {ExampleModule} from '../example/example-module';
|
||||
import {PopoverEditDemo} from './popover-edit-demo';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
ExampleModule,
|
||||
FormsModule,
|
||||
RouterModule.forChild([{path: '', component: PopoverEditDemo}]),
|
||||
],
|
||||
declarations: [PopoverEditDemo],
|
||||
})
|
||||
export class PopoverEditDemoModule {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {EXAMPLE_COMPONENTS} from '@angular/material-examples';
|
||||
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
template: '<material-example-list [ids]="examples"></material-example-list>',
|
||||
})
|
||||
export class PopoverEditDemo {
|
||||
examples = Object.keys(EXAMPLE_COMPONENTS)
|
||||
.filter(example => example.startsWith('popover-edit-') ||
|
||||
example.startsWith('cdk-popover-edit-'));
|
||||
}
|
||||
@@ -66,6 +66,8 @@ System.config({
|
||||
|
||||
'@angular/cdk-experimental/scrolling': 'dist/packages/cdk-experimental/scrolling/index.js',
|
||||
'@angular/cdk-experimental/dialog': 'dist/packages/cdk-experimental/dialog/index.js',
|
||||
'@angular/cdk-experimental/popover-edit':
|
||||
'dist/packages/cdk-experimental/popover-edit/index.js',
|
||||
|
||||
'@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js',
|
||||
'@angular/material/bottom-sheet': 'dist/packages/material/bottom-sheet/index.js',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package(default_visibility=["//visibility:public"])
|
||||
|
||||
load("//:packages.bzl", "CDK_TARGETS", "MATERIAL_TARGETS", "ROLLUP_GLOBALS")
|
||||
load("//:packages.bzl", "CDK_TARGETS", "CDK_EXPERIMENTAL_TARGETS", "MATERIAL_TARGETS", "ROLLUP_GLOBALS")
|
||||
load("//tools:defaults.bzl", "ng_module", "ng_package")
|
||||
load("//tools/highlight-files:index.bzl", "highlight_files")
|
||||
load("//tools/package-docs-content:index.bzl", "package_docs_content")
|
||||
@@ -17,7 +17,7 @@ ng_module(
|
||||
"@npm//@angular/forms",
|
||||
"@npm//moment",
|
||||
"//src/material-moment-adapter",
|
||||
] + CDK_TARGETS + MATERIAL_TARGETS,
|
||||
] + CDK_TARGETS + CDK_EXPERIMENTAL_TARGETS + MATERIAL_TARGETS,
|
||||
# Specify the tsconfig that is also used by Gulp. We need to explicitly use this tsconfig
|
||||
# because in order to import Moment with TypeScript, some specific options need to be set.
|
||||
tsconfig = ":tsconfig-build.json",
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Add basic flex styling so that the cells evenly space themselves in the row.
|
||||
*/
|
||||
.example-table cdk-row, cdk-header-row, cdk-footer-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.example-table cdk-cell, cdk-header-cell, cdk-footer-cell {
|
||||
flex: 1;
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<cdk-table class="example-table" editable [dataSource]="dataSource">
|
||||
<!--
|
||||
This edit lens is specified outside of the cell and must explicitly declare
|
||||
its context. It could be reused in multiple cells.
|
||||
-->
|
||||
<ng-template #weightEdit let-element>
|
||||
<div style="background-color: white;">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
(ngSubmit)="onSubmitWeight(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedWeightValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedWeightValues.set(element, $event)">
|
||||
Edit b:
|
||||
<input type="number" [ngModel]="element.weight" name="weight" required>
|
||||
<br>
|
||||
<button type="submit">Confirm</button>
|
||||
<button cdkEditRevert>Revert</button>
|
||||
<button cdkEditClose>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- Position Column -->
|
||||
<ng-container cdkColumnDef="position">
|
||||
<cdk-header-cell *cdkHeaderCellDef> No. </cdk-header-cell>
|
||||
<cdk-cell *cdkCellDef="let element"> {{element.position}} </cdk-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container cdkColumnDef="name">
|
||||
<cdk-header-cell *cdkHeaderCellDef> Name </cdk-header-cell>
|
||||
<cdk-cell *cdkCellDef="let element"
|
||||
[cdkPopoverEdit]="nameEdit">
|
||||
{{element.name}}
|
||||
|
||||
<!-- This edit is defined in the cell and can implicitly access element -->
|
||||
<ng-template #nameEdit>
|
||||
<div style="background-color: white;">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
(ngSubmit)="onSubmitName(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedNameValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedNameValues.set(element, $event)">
|
||||
Edit a:
|
||||
<input [ngModel]="element.name" name="name" required>
|
||||
<br>
|
||||
<button type="submit">Confirm</button>
|
||||
<button cdkEditRevert>Revert</button>
|
||||
<button cdkEditClose>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<span *cdkRowHoverContent>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</cdk-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Weight Column -->
|
||||
<ng-container cdkColumnDef="weight">
|
||||
<cdk-header-cell *cdkHeaderCellDef> Weight </cdk-header-cell>
|
||||
<cdk-cell *cdkCellDef="let element"
|
||||
[cdkPopoverEdit]="weightEdit" [cdkPopoverEditContext]="element">
|
||||
{{element.weight}}
|
||||
|
||||
<span *cdkRowHoverContent>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</cdk-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Symbol Column -->
|
||||
<ng-container cdkColumnDef="symbol">
|
||||
<cdk-header-cell *cdkHeaderCellDef> Symbol </cdk-header-cell>
|
||||
<cdk-cell *cdkCellDef="let element"> {{element.symbol}} </cdk-cell>
|
||||
</ng-container>
|
||||
|
||||
<cdk-header-row *cdkHeaderRowDef="displayedColumns"></cdk-header-row>
|
||||
<cdk-row *cdkRowDef="let row; columns: displayedColumns;"></cdk-row>
|
||||
</cdk-table>
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import {DataSource} from '@angular/cdk/collections';
|
||||
import {Component} from '@angular/core';
|
||||
import {NgForm} from '@angular/forms';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
export interface PeriodicElement {
|
||||
name: string;
|
||||
position: number;
|
||||
weight: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const ELEMENT_DATA: PeriodicElement[] = [
|
||||
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
|
||||
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
|
||||
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
|
||||
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
|
||||
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
|
||||
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
|
||||
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
|
||||
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
|
||||
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
|
||||
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
|
||||
{position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'},
|
||||
{position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'},
|
||||
{position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'},
|
||||
{position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'},
|
||||
{position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'},
|
||||
{position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'},
|
||||
{position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'},
|
||||
{position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'},
|
||||
{position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'},
|
||||
{position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'},
|
||||
];
|
||||
|
||||
/**
|
||||
* @title CDK Popover Edit on a flex cdk-table.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cdk-popover-edit-cdk-table-flex-example',
|
||||
styleUrls: ['cdk-popover-edit-cdk-table-flex-example.css'],
|
||||
templateUrl: 'cdk-popover-edit-cdk-table-flex-example.html',
|
||||
})
|
||||
export class CdkPopoverEditCdkTableFlexExample {
|
||||
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
|
||||
dataSource = new ExampleDataSource();
|
||||
|
||||
readonly preservedNameValues = new WeakMap<PeriodicElement, any>();
|
||||
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
|
||||
|
||||
onSubmitName(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
element.name = f.value.name;
|
||||
}
|
||||
|
||||
onSubmitWeight(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
element.weight = f.value.weight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data source to provide what data should be rendered in the table. Note that the data source
|
||||
* can retrieve its data in any way. In this case, the data source is provided a reference
|
||||
* to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
|
||||
* the underlying data. Instead, it only needs to take the data and send the table exactly what
|
||||
* should be rendered.
|
||||
*/
|
||||
export class ExampleDataSource extends DataSource<PeriodicElement> {
|
||||
/** Stream of data that is provided to the table. */
|
||||
data = new BehaviorSubject<PeriodicElement[]>(ELEMENT_DATA);
|
||||
|
||||
/** Connect function called by the table to retrieve one stream containing the data to render. */
|
||||
connect(): Observable<PeriodicElement[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
.example-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example-table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.example-table td,
|
||||
.example-table th {
|
||||
width: 25%;
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<table class="example-table" cdk-table editable [dataSource]="dataSource">
|
||||
<!--
|
||||
This edit lens is specified outside of the cell and must explicitly declare
|
||||
its context. It could be reused in multiple cells.
|
||||
-->
|
||||
<ng-template #weightEdit let-element>
|
||||
<div style="background-color: white;">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
(ngSubmit)="onSubmitWeight(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedWeightValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedWeightValues.set(element, $event)">
|
||||
Edit b:
|
||||
<input type="number" [ngModel]="element.weight" name="weight" required>
|
||||
<br>
|
||||
<button type="submit">Confirm</button>
|
||||
<button cdkEditRevert cdkEditClose>Revert and close</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- Position Column -->
|
||||
<ng-container cdkColumnDef="position">
|
||||
<th cdk-header-cell *cdkHeaderCellDef> No. </th>
|
||||
<td cdk-cell *cdkCellDef="let element"> {{element.position}} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container cdkColumnDef="name">
|
||||
<th cdk-header-cell *cdkHeaderCellDef> Name </th>
|
||||
<td cdk-cell *cdkCellDef="let element"
|
||||
[cdkPopoverEdit]="nameEdit">
|
||||
{{element.name}}
|
||||
|
||||
<!-- This edit is defined in the cell and can implicitly access element -->
|
||||
<ng-template #nameEdit>
|
||||
<div style="background-color: white;">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
(ngSubmit)="onSubmitName(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedNameValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedNameValues.set(element, $event)">
|
||||
Edit a:
|
||||
<input [ngModel]="element.name" name="name" required>
|
||||
<br>
|
||||
<button type="submit">Confirm</button>
|
||||
<button cdkEditRevert>Revert</button>
|
||||
<button cdkEditClose>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<span *cdkRowHoverContent>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Weight Column -->
|
||||
<ng-container cdkColumnDef="weight">
|
||||
<th cdk-header-cell *cdkHeaderCellDef> Weight </th>
|
||||
<td cdk-cell *cdkCellDef="let element"
|
||||
[cdkPopoverEdit]="weightEdit" [cdkPopoverEditContext]="element">
|
||||
{{element.weight}}
|
||||
|
||||
<span *cdkRowHoverContent>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Symbol Column -->
|
||||
<ng-container cdkColumnDef="symbol">
|
||||
<th cdk-header-cell *cdkHeaderCellDef> Symbol </th>
|
||||
<td cdk-cell *cdkCellDef="let element"> {{element.symbol}} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr cdk-header-row *cdkHeaderRowDef="displayedColumns"></tr>
|
||||
<tr cdk-row *cdkRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import {DataSource} from '@angular/cdk/collections';
|
||||
import {Component} from '@angular/core';
|
||||
import {NgForm} from '@angular/forms';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
export interface PeriodicElement {
|
||||
name: string;
|
||||
position: number;
|
||||
weight: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const ELEMENT_DATA: PeriodicElement[] = [
|
||||
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
|
||||
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
|
||||
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
|
||||
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
|
||||
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
|
||||
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
|
||||
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
|
||||
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
|
||||
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
|
||||
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
|
||||
{position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'},
|
||||
{position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'},
|
||||
{position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'},
|
||||
{position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'},
|
||||
{position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'},
|
||||
{position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'},
|
||||
{position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'},
|
||||
{position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'},
|
||||
{position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'},
|
||||
{position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'},
|
||||
];
|
||||
|
||||
/**
|
||||
* @title CDK Popover Edit on a CDK data-table
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cdk-popover-edit-cdk-table-example',
|
||||
styleUrls: ['cdk-popover-edit-cdk-table-example.css'],
|
||||
templateUrl: 'cdk-popover-edit-cdk-table-example.html',
|
||||
})
|
||||
export class CdkPopoverEditCdkTableExample {
|
||||
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
|
||||
dataSource = new ExampleDataSource();
|
||||
|
||||
readonly preservedNameValues = new WeakMap<PeriodicElement, any>();
|
||||
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
|
||||
|
||||
onSubmitName(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
element.name = f.value.name;
|
||||
}
|
||||
|
||||
onSubmitWeight(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
element.weight = f.value.weight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data source to provide what data should be rendered in the table. Note that the data source
|
||||
* can retrieve its data in any way. In this case, the data source is provided a reference
|
||||
* to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
|
||||
* the underlying data. Instead, it only needs to take the data and send the table exactly what
|
||||
* should be rendered.
|
||||
*/
|
||||
export class ExampleDataSource extends DataSource<PeriodicElement> {
|
||||
/** Stream of data that is provided to the table. */
|
||||
data = new BehaviorSubject<PeriodicElement[]>(ELEMENT_DATA);
|
||||
|
||||
/** Connect function called by the table to retrieve one stream containing the data to render. */
|
||||
connect(): Observable<PeriodicElement[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
.example-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example-table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.example-table td,
|
||||
.example-table th {
|
||||
width: 25%;
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<table editable class="example-table">
|
||||
<!--
|
||||
This edit lens is specified outside of the cell and must explicitly declare
|
||||
its context. It could be reused in multiple cells.
|
||||
-->
|
||||
<ng-template #weightEdit let-element>
|
||||
<div style="background-color: white;">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
(ngSubmit)="onSubmitWeight(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedWeightValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedWeightValues.set(element, $event)">
|
||||
Edit b:
|
||||
<input type="number" [ngModel]="element.weight" name="weight" required>
|
||||
<br>
|
||||
<button type="submit">Confirm</button>
|
||||
<button cdkEditRevert>Revert</button>
|
||||
<button cdkEditClose>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<tr>
|
||||
<th> No. </th>
|
||||
<th> Name </th>
|
||||
<th> Weight </th>
|
||||
<th> Symbol </th>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let element of elements">
|
||||
<td> {{element.position}} </td>
|
||||
|
||||
<td [cdkPopoverEdit]="nameEdit">
|
||||
{{element.name}}
|
||||
|
||||
<!-- This edit is defined in the cell and can implicitly access element -->
|
||||
<ng-template #nameEdit>
|
||||
<div style="background-color: white;">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
(ngSubmit)="onSubmitName(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedNameValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedNameValues.set(element, $event)">
|
||||
Edit a:
|
||||
<input [ngModel]="element.name" name="name" required>
|
||||
<br>
|
||||
<button type="submit">Confirm</button>
|
||||
<button cdkEditRevert>Revert</button>
|
||||
<button cdkEditClose>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<span *cdkRowHoverContent>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td [cdkPopoverEdit]="weightEdit" [cdkPopoverEditContext]="element">
|
||||
{{element.weight}}
|
||||
|
||||
<span *cdkRowHoverContent>
|
||||
<button cdkEditOpen>Edit</button>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td> {{element.symbol}} </td>
|
||||
</tr>
|
||||
</table>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {NgForm} from '@angular/forms';
|
||||
|
||||
export interface PeriodicElement {
|
||||
name: string;
|
||||
position: number;
|
||||
weight: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const ELEMENT_DATA: PeriodicElement[] = [
|
||||
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
|
||||
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
|
||||
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
|
||||
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
|
||||
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
|
||||
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
|
||||
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
|
||||
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
|
||||
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
|
||||
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
|
||||
{position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'},
|
||||
{position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'},
|
||||
{position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'},
|
||||
{position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'},
|
||||
{position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'},
|
||||
{position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'},
|
||||
{position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'},
|
||||
{position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'},
|
||||
{position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'},
|
||||
{position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'},
|
||||
];
|
||||
|
||||
/**
|
||||
* @title CDK Popover Edit on an HTML data-table
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cdk-popover-edit-vanilla-table-example',
|
||||
styleUrls: ['cdk-popover-edit-vanilla-table-example.css'],
|
||||
templateUrl: 'cdk-popover-edit-vanilla-table-example.html',
|
||||
})
|
||||
export class CdkPopoverEditVanillaTableExample {
|
||||
readonly preservedNameValues = new WeakMap<PeriodicElement, any>();
|
||||
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
|
||||
|
||||
readonly elements = ELEMENT_DATA;
|
||||
|
||||
onSubmitName(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
element.name = f.value.name;
|
||||
}
|
||||
|
||||
onSubmitWeight(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
element.weight = f.value.weight;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {NgModule} from '@angular/core';
|
||||
|
||||
import {ScrollingModule} from '@angular/cdk/scrolling';
|
||||
import {A11yModule} from '@angular/cdk/a11y';
|
||||
import {CdkPopoverEditModule} from '@angular/cdk-experimental/popover-edit';
|
||||
import {CdkTableModule} from '@angular/cdk/table';
|
||||
import {CdkTreeModule} from '@angular/cdk/tree';
|
||||
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
@NgModule({
|
||||
imports: [
|
||||
A11yModule,
|
||||
CdkPopoverEditModule,
|
||||
CdkTableModule,
|
||||
CdkTreeModule,
|
||||
CdkStepperModule,
|
||||
@@ -66,6 +68,7 @@ import {
|
||||
],
|
||||
exports: [
|
||||
A11yModule,
|
||||
CdkPopoverEditModule,
|
||||
CdkTableModule,
|
||||
CdkTreeModule,
|
||||
CdkStepperModule,
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"@angular/material/*": ["../../dist/packages/material/*"],
|
||||
"@angular/material": ["../../dist/packages/material"],
|
||||
"@angular/material-moment-adapter": ["../../dist/packages/material-moment-adapter"],
|
||||
"@angular/cdk/*": ["../../dist/packages/cdk/*"]
|
||||
"@angular/cdk/*": ["../../dist/packages/cdk/*"],
|
||||
"@angular/cdk-experimental/*": ["../../dist/packages/cdk-experimental/*"],
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
Reference in New Issue
Block a user