lwc icon indicating copy to clipboard operation
lwc copied to clipboard

Allow access to Slot elements from the component

Open AllanOricil opened this issue 3 years ago • 4 comments

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>

image

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.

AllanOricil avatar Nov 13 '20 10:11 AllanOricil

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.

Bartheleway avatar Feb 03 '21 13:02 Bartheleway

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 avatar Aug 08 '22 23:08 nolanlawson

@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

AllanOricil avatar Aug 09 '22 10:08 AllanOricil

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.

nolanlawson avatar Aug 09 '22 18:08 nolanlawson

Delivered 23 with scoped slots? @nolanlawson I could not find docs saying that this.slots is accessible in the parent js

AllanOricil avatar May 14 '23 16:05 AllanOricil

@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.

nolanlawson avatar May 15 '23 17:05 nolanlawson