Skip to content

Commit 8a4bed5

Browse files
crisbetojelbourn
authored andcommitted
fix(drag-drop): error if custom preview or placeholder node is not an element (#16409)
When the custom preview or placeholder root node is provided we assume that it's an `HTMLElement` and we try to set some styles on it, however it's possible for it to be a text node (e.g. `<ng-template cdkDragPreview>Hello</ng-template>`) which will cause an error to be thrown. These changes wrap the node in a `div` if it's not an element node.
1 parent 357da7b commit 8a4bed5

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2720,6 +2720,21 @@ describe('CdkDrag', () => {
27202720
expect(preview.style.transform).toBe('translate3d(100px, 50px, 0px)');
27212721
}));
27222722

2723+
it('should not throw when custom preview only has text', fakeAsync(() => {
2724+
const fixture = createComponent(DraggableInDropZoneWithCustomTextOnlyPreview);
2725+
fixture.detectChanges();
2726+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2727+
2728+
expect(() => {
2729+
startDraggingViaMouse(fixture, item);
2730+
}).not.toThrow();
2731+
2732+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2733+
2734+
expect(preview).toBeTruthy();
2735+
expect(preview.textContent!.trim()).toContain('Hello One');
2736+
}));
2737+
27232738
it('should be able to customize the placeholder', fakeAsync(() => {
27242739
const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder);
27252740
fixture.detectChanges();
@@ -2753,6 +2768,21 @@ describe('CdkDrag', () => {
27532768
expect(placeholder.textContent!.trim()).not.toContain('Custom placeholder');
27542769
}));
27552770

2771+
it('should not throw when custom placeholder only has text', fakeAsync(() => {
2772+
const fixture = createComponent(DraggableInDropZoneWithCustomTextOnlyPlaceholder);
2773+
fixture.detectChanges();
2774+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2775+
2776+
expect(() => {
2777+
startDraggingViaMouse(fixture, item);
2778+
}).not.toThrow();
2779+
2780+
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
2781+
2782+
expect(placeholder).toBeTruthy();
2783+
expect(placeholder.textContent!.trim()).toContain('Hello One');
2784+
}));
2785+
27562786
it('should clear the `transform` value from siblings when item is dropped`', fakeAsync(() => {
27572787
const fixture = createComponent(DraggableInDropZone);
27582788
fixture.detectChanges();
@@ -4497,6 +4527,28 @@ class DraggableInDropZoneWithCustomPreview {
44974527
}
44984528

44994529

4530+
@Component({
4531+
template: `
4532+
<div cdkDropList style="width: 100px; background: pink;">
4533+
<div
4534+
*ngFor="let item of items"
4535+
cdkDrag
4536+
[cdkDragConstrainPosition]="constrainPosition"
4537+
[cdkDragBoundary]="boundarySelector"
4538+
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
4539+
{{item}}
4540+
<ng-template cdkDragPreview>Hello {{item}}</ng-template>
4541+
</div>
4542+
</div>
4543+
`
4544+
})
4545+
class DraggableInDropZoneWithCustomTextOnlyPreview {
4546+
@ViewChild(CdkDropList, {static: false}) dropInstance: CdkDropList;
4547+
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
4548+
items = ['Zero', 'One', 'Two', 'Three'];
4549+
}
4550+
4551+
45004552
@Component({
45014553
template: `
45024554
<div cdkDropList style="width: 100px; background: pink;">
@@ -4516,6 +4568,22 @@ class DraggableInDropZoneWithCustomPlaceholder {
45164568
renderPlaceholder = true;
45174569
}
45184570

4571+
@Component({
4572+
template: `
4573+
<div cdkDropList style="width: 100px; background: pink;">
4574+
<div *ngFor="let item of items" cdkDrag
4575+
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
4576+
{{item}}
4577+
<ng-template cdkDragPlaceholder>Hello {{item}}</ng-template>
4578+
</div>
4579+
</div>
4580+
`
4581+
})
4582+
class DraggableInDropZoneWithCustomTextOnlyPlaceholder {
4583+
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
4584+
items = ['Zero', 'One', 'Two', 'Three'];
4585+
}
4586+
45194587
const CONNECTED_DROP_ZONES_STYLES = [`
45204588
.cdk-drop-list {
45214589
display: block;

src/cdk/drag-drop/drag-ref.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,7 @@ export class DragRef<T = any> {
849849
if (previewTemplate) {
850850
const viewRef = previewConfig!.viewContainer.createEmbeddedView(previewTemplate,
851851
previewConfig!.context);
852-
preview = viewRef.rootNodes[0];
852+
preview = getRootNode(viewRef, this._document);
853853
this._previewRef = viewRef;
854854
preview.style.transform =
855855
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
@@ -941,7 +941,7 @@ export class DragRef<T = any> {
941941
placeholderTemplate,
942942
placeholderConfig!.context
943943
);
944-
placeholder = this._placeholderRef.rootNodes[0];
944+
placeholder = getRootNode(this._placeholderRef, this._document);
945945
} else {
946946
placeholder = deepCloneNode(this._rootElement);
947947
}
@@ -1231,3 +1231,19 @@ function getPreviewInsertionPoint(documentRef: any): HTMLElement {
12311231
documentRef.msFullscreenElement ||
12321232
documentRef.body;
12331233
}
1234+
1235+
/**
1236+
* Gets the root HTML element of an embedded view.
1237+
* If the root is not an HTML element it gets wrapped in one.
1238+
*/
1239+
function getRootNode(viewRef: EmbeddedViewRef<any>, _document: Document): HTMLElement {
1240+
const rootNode: Node = viewRef.rootNodes[0];
1241+
1242+
if (rootNode.nodeType !== _document.ELEMENT_NODE) {
1243+
const wrapper = _document.createElement('div');
1244+
wrapper.appendChild(rootNode);
1245+
return wrapper;
1246+
}
1247+
1248+
return rootNode as HTMLElement;
1249+
}

0 commit comments

Comments
 (0)