lwc
lwc copied to clipboard
Allow access to Slot elements from the component
Is your feature request related to a problem? Please describe. Yes, I need to create a component that makes everything that is passed inside of it draggable.
//wrapper to show the way I want to make a list of something that all items on it should be draggable.
<c-drag-n-drop>
<template for:each={_entries} for:item="entry" for:index="entryIndex">
<anything-to-be-draggable key={entryIndex}></anything-to-be-draggable>
</template>
</c-drag-n-drop>
//c-drag-n-drop
<template>
<div class={containerClass}>
<slot></slot>
</div>
</template>
import { api, LightningElement } from 'lwc';
export default class DceDragAndDrop extends LightningElement {
connectedCallback(){
//iterate over each slot element and add these handlers to each one of them if it does not exist already
//Like
this.slots.forEach((slot) => {
slot.addEventListener('dragstart', this.handleDragStart);
slot.addEventListener('dragend', this.handleDragEnd);
slot.addEventListener('dragover', this.handleDragOver);
slot.addEventListener('dragenter', this.handleDragEnter);
slot.addEventListener('dragleave', this.hanldeDragLeave);
}
// or we can add it to directly to template like I show below
}
//drag and drop handlers
handleDragStart(e){
//handle drag start
}
handleDragEnd(e){
//handle drag end
}
handleDragOver(e) {
e.preventDefault();
return false;
}
handleDragEnter(e) {
e.target.classList.add('over');
}
handleDragLeave(e) {
e.target.classList.remove('over');
}
handleDrop(e){
e.stopPropagation();
//switch elements
return false;
}
get containerClass(){
return 'container ' + (this.horizontal ? 'horizontal' : 'vertical');
}
}
I built this component which uses drag n drop events and I would like to be used to make anything drag and drop. At the moment I could only hardcode a checkbox input inside of it. But with access to the slots elements I would be able to make anything drag n drop. I would be able to do a for:each on the slots list and add the events directly on the template and add it to any slot that it was passed from a parent, like:
<template>
<div class={containerClass}>
<template for:each={slots} for:item="slot" for:index="entryIndex">
<slot
data-id={entryIndex}
key={slot.data.name}
draggable="true"
ondragstart={handleDragStart}
ondragend={handleDragEnd}
ondragover={handleDragOver}
ondragenter={handleDragEnter}
ondragleave={handleDragLeave}
ondrop={handleDrop}
class="entry"
>
</slot>
</div>
</template>
This component is not reusable, since it has the input component hardcoded there :/
//drag-n-drop-checkbox-input
<template>
<div class={containerClass}>
<template for:each={_entries} for:item="entry" for:index="entryIndex">
<div
data-id={entryIndex}
key={entry.label}
draggable="true"
ondragstart={handleDragStart}
ondragend={handleDragEnd}
ondragover={handleDragOver}
ondragenter={handleDragEnter}
ondragleave={handleDragLeave}
ondrop={handleDrop}
class="entry"
>
<c-dce-checkbox-input
data-id={entryIndex}
checked={entry.isSelected}
label={entry.label}
onchange={handleCheckboxChange}
>
</c-dce-checkbox-input>
</div>
</template>
</div>
</template>
import { api, LightningElement } from "lwc";
export default class DceDragAndDrop extends LightningElement {
@api
get entries() {
return this._entries;
}
//for some reason [...entries] is throwing an exception
//also, this way it allows us to not pass isSelected in the object.
set entries(entries) {
this._entries = entries.map((entry) => {
return {
label: entry.label || "label 1",
isSelected: entry.isSelected === true
};
});
}
@api
horizontal = false;
_entries = [];
_draggingElementIndex = -1;
_counter = 0;
//input handler
handleCheckboxChange(e) {
this._entries[e.target.dataset.id].isSelected = e.detail.value;
this._dispatchEvent();
}
//drag and drop handlers
handleDragStart(e) {
e.target.style.opacity = 0.4;
this._draggingElementIndex = e.target.dataset.id;
}
handleDragEnd(e) {
e.target.style.opacity = 1;
const elements = e.currentTarget.parentElement.children;
for (let element of elements) {
element.classList.remove("over");
}
}
handleDragOver(e) {
e.preventDefault();
return false;
}
handleDragEnter(e) {
e.preventDefault();
e.target.classList.add("over");
}
handleDragLeave(e) {
const rect = e.currentTarget.getBoundingClientRect();
// Check the mouseEvent coordinates are outside of the rectangle
if (
e.x > rect.left + rect.width ||
e.x < rect.left ||
e.y > rect.top + rect.height ||
e.y < rect.top
) {
e.target.classList.remove("over");
}
}
handleDrop(e) {
e.stopPropagation();
const origin = this._draggingElementIndex;
const destination = e.currentTarget.dataset.id;
if (origin !== destination) {
const destinationBackup = { ...this._entries[destination] };
this._entries[destination] = { ...this._entries[origin] };
this._entries[origin] = destinationBackup;
this._entries = [...this._entries];
this._dispatchEvent();
}
return false;
}
get containerClass() {
return "container " + (this.horizontal ? "horizontal" : "vertical");
}
_dispatchEvent() {
const changeEvent = new CustomEvent("change", {
detail: { entries: this._entries }
});
this.dispatchEvent(changeEvent);
}
}
.container {
display: grid;
gap: 10px;
}
.horizontal {
grid-auto-flow: column !important;
}
.vertical {
grid-auto-flow: row !important;
}
/* We need to add the HPE Colors */
.entry {
border: 3px solid transparent;
background-color: #ddd;
border-radius: 0.5em;
padding: 10px;
cursor: move;
}
/* This is to change the component when the dragging element is hovering it */
.entry.over {
border: 3px dotted #666;
}
Describe the solution you'd like On Vue Js we have access to Slot elements from the parent, which allows us to iterate over them to add custom css and event handlers.
Describe alternatives you've considered
Use lwc:dom
and write the component from the scratch to have access to the dom as I want. But then I loose everything lwc has to offer.
Hi @AllanOricil, I believe your issue is somehow similar to mine #2218 in a more complex example. It doesn't seems possible currently to have slot in loop.
This sounds like a request for what we're calling "scoped slots": https://github.com/salesforce/lwc-rfcs/pull/63 . The reason I think so is because you're wanting to have slots inside of a for-loop and to pass for-loop-specific information into those slots (e.g. the current iteration index).
Closing as a duplicate of https://github.com/salesforce/lwc/issues/210, please reopen if I'm mistaken.
@nolanlawson it is related, but I'm not sure if #210 would also expose this.slots
in the parent js class. If it does, then you can keep this closed
Hm, you're right, the proposal doesn't seem to allow programmatic access to the <slot>
elements; in fact I believe they are not true <slot>
s because native shadow DOM does not support scoped slots. Template refs (#2690) would also not address this since they are not true <slot>
s.
Delivered 23 with scoped slots? @nolanlawson
I could not find docs saying that this.slots
is accessible in the parent js
@AllanOricil It's true that we don't provide explicit access to slots. However, in native shadow you should be able to do this.template.querySelectorAll('slot')
which gives the same thing.
For scoped slots, those are light DOM slots, and in the case of light DOM slots, I don't even know what it would mean to expose this.slots
, since the slots are "virtual" and only really exist in the compiler. At runtime, there is no <slot>
for light DOM.