EDIT 2: The issue has been fixed by the amazing CDK Team!
EDIT: As this seems to be quite strange behaviour, I have for now added an issue in the official CDK repository. Feel free to suggest any ideas or workourounds, however.
I have different types of elements in my *cdkDropList
that need different placholder hights when dragging them around. Since the array has objects with a property type
, I added those types as CSS classes and tried to add them dynamically using [ngClass]
. However, these dynamically generated classes behave differently than when I'm setting them as "regular" CSS classes.
This is what happens, when I set the classes dynamically:
The placeholder and the elements in the dropList
overlap. Here's the relevant code:
example.component.ts
contentItems: ContentItem[] = [
{ type: 'text', /* more props */ },
{ type: 'text', /* more props */ },
{ type: 'image', /* more props */ }
];
example.component.html
<div *ngFor="let item of contentItems" class="editor-item" cdkDrag>
<div [ngClass]="['dropzone-placeholder', item.type]" *cdkDragPlaceholder>
<p>{{ 'EDITOR.INSERT HERE' | translate }}</p>
</div>
<app-language-tab-editor *ngIf="item.type === 'text'"></app-language-tab-editor>
<app-image-upload *ngIf="item.type === 'image'"></app-image-upload>
</div>
example.component.scss
$dropzone-placeholder-dark: #00973B;
$dropzone-placeholder-light: #00973B0D;
$text-placeholder-height: 135px;
$image-placeholder-height: 375px;
.dropzone-placeholder {
border: 1px dashed $dropzone-placeholder-dark;
color: $dropzone-placeholder-dark;
background: $dropzone-placeholder-light;
&.text {
height: $text-placeholder-height;
}
&.image {
height: $image-placeholder-height;
}
}
I currently only have two different types, but the goal is to make it easily expandable to add more later on. I have also already tried to instead use class="dropzone-placeholder {{ item.type }}"
as well as [class]="'dropzone-placeholder ' + item.type"
, to no avail.
After further testing I have also found out, that it generally doesn't work using [ngClass]
, even if we don't use a variable. Using [ngClass]="['dropzone-placeholder', 'text']"
didn't work either.
This is the expected behaviour:
The placeholder and the elements in the dropList
don't overlap and are instead placed properly below each other. This behaviour can currently only be achieved by setting the classes regularily, but the HTML is rather unpleasant to look at, since the code would get messily redundant in the future.
example.component.html
<div *ngFor="let item of contentItems" class="editor-item" cdkDrag>
<div *ngIf="item.type === 'text'">
<div class="dropzone-placeholder reorder text" *cdkDragPlaceholder>
<p>{{ 'EDITOR.INSERT HERE' | translate }}</p>
</div>
</div>
<div *ngIf="item.type === 'image'">
<div class="dropzone-placeholder reorder image" *cdkDragPlaceholder>
<p>{{ 'EDITOR.INSERT HERE' | translate }}</p>
</div>
</div>
<app-language-tab-editor *ngIf="item.type === 'text'"></app-language-tab-editor>
<app-image-upload *ngIf="item.type === 'image'"></app-image-upload>
</div>
Why is the CDK behaving differently when the classes are not set the traditional way? And how can I avoid writing the redundant workaround mentioned above?
UPDATE: The issue has since been fixed natively in the CDK-Package, rendering the workaround obsolete.
As pointed out in this answer to the aforementioned GitHub issue by @Achilles1515, there is currently a workaround using patched code in the TS file. Definitely keep an eye on any updates in the CDK that fix this natively.
Here's the relevant code:
import { CdkDragDrop, DragRef, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, EmbeddedViewRef} from '@angular/core';
DragRef.prototype._createPlaceholderElement = function(): HTMLElement {
const placeholderConfig = this._placeholderTemplate;
const placeholderTemplate = placeholderConfig
? placeholderConfig.template
: null;
let placeholder: HTMLElement;
if (placeholderTemplate) {
this._placeholderRef = placeholderConfig!.viewContainer.createEmbeddedView(
placeholderTemplate,
placeholderConfig!.context
);
this._placeholderRef.detectChanges(); // This is the missing line in the CDK
placeholder = getRootNode(this._placeholderRef, this._document);
} else {
placeholder = deepCloneNode(this._rootElement);
}
placeholder.classList.add('cdk-drag-placeholder');
return placeholder;
};
/** Creates a deep clone of an element. */
function deepCloneNode(node: HTMLElement): HTMLElement {
const clone = node.cloneNode(true) as HTMLElement;
const descendantsWithId = clone.querySelectorAll('[id]');
const descendantCanvases = node.querySelectorAll('canvas');
// Remove the `id` to avoid having multiple elements with the same id on the page.
clone.removeAttribute('id');
for (let i = 0; i < descendantsWithId.length; i++) {
descendantsWithId[i].removeAttribute('id');
}
// `cloneNode` won't transfer the content of `canvas` elements so we have to do it ourselves.
// We match up the cloned canvas to their sources using their index in the DOM.
if (descendantCanvases.length) {
const cloneCanvases = clone.querySelectorAll('canvas');
for (let i = 0; i < descendantCanvases.length; i++) {
const correspondingCloneContext = cloneCanvases[i].getContext('2d');
if (correspondingCloneContext) {
correspondingCloneContext.drawImage(descendantCanvases[i], 0, 0);
}
}
}
return clone;
}
function getRootNode(
viewRef: EmbeddedViewRef<any>,
_document: Document
): HTMLElement {
const rootNode: Node = viewRef.rootNodes[0];
if (rootNode.nodeType !== _document.ELEMENT_NODE) {
const wrapper = _document.createElement('div');
wrapper.appendChild(rootNode);
return wrapper;
}
return rootNode as HTMLElement;
}
Adding this to my code fixed the strange behaviour with [ngClass]
.