ThibaultJanBeyer / DragSelect

An easy JavaScript library for selecting and moving elements. With no dependencies. Drag-Select & Drag-And-Drop. – Examples:
https://dragselect.com/
Other
697 stars 80 forks source link

Document how to use with Vue.js #178

Open danielxvu opened 1 year ago

danielxvu commented 1 year ago

Is your feature request related to a problem? Please describe.

Official documentation for usage with Vue.js projects is lacking.

Describe the solution you'd like A guided example on par with the one for React.

Describe alternatives you've considered

I would be happy to contribute a pull request if you are open to reviewing and accepting it.

Additional context

Providing examples of composables based on this library would be ideal, similar to how the React guided example documents how to use this library with useEffect.

ThibaultJanBeyer commented 1 year ago

Hi @danielxvu yes that would be awesome! Unfortunately I have absolutely no knowledge of Vue myself. If you’re familiar with it would be great if you could help out here 🥂

justinross commented 1 year ago

This is a very rudimentary Vue composable I threw together in Typescript (barely) that's generally working for a project I'm working on. Bear in mind I'm just learning Vue, so I'll be very happy to take feedback and constructive criticism.

Essentially, it creates a new DragSelect instance, provided an array of selectables and a selectable area. It tries to handle allowing for selectables and area to be Vue ref()'s (so you can use Vue template refs), straight HTML elements, or Vue components. I haven't done a ton of testing on that front to make sure it works entirely, but it seems to work alright.

If Vue reactive objects get passed in, it should also set up watchers to update the DragSelect instance when they're updated. This is mostly to alleviate issues with template refs getting called before they're available in the template. There's probably a better way to handle this, but it works for now.

I'll try to keep this updated as I find issues/improvements.

/edit: Also, this is a really basic solution. It would probably be beneficial to return an object with the DS instance so that the various DragSelect methods are exposed. This was just an easy way to get it up and running for my needs.

import { unref, isRef, ref, onMounted, onUnmounted, watchEffect } from 'vue'
import DragSelect from 'dragselect'

export function useDragSelect (selectables, area) {
  // state encapsulated and managed by the composable
  const selected = ref([])
  let dragSelectInstance: DragSelect

  function initDragSelect () {
    // This is all doing some checking/cleanup to allow for different types of input
    // selectables is always an array, but it can be an array of HTML elements,
    // a vue ref() of HTML elements, or an array of Vue components
    // Similarly, area can be either an HTML element or Vue component, ref() or not

    if (unref(selectables) && unref(area)) {
      const selectableElementArray = unref(selectables).map(el => {
        if (el.__isVue) {
          return el.$el
        } else {
          return unref(el)
        }
      })

      let areaElement
      if (unref(area).__isVue) {
        areaElement = unref(area).$el
      } else {
        areaElement = unref(area)
      }

      if (dragSelectInstance instanceof DragSelect) {
        dragSelectInstance.stop()
        dragSelectInstance.setSettings({
          selectables: selectableElementArray,
          area: areaElement,
          draggability: false
        })
        dragSelectInstance.start()
      } else {
        dragSelectInstance = new DragSelect({
          selectables: selectableElementArray,
          area: areaElement,
          draggability: false
        })
      }
    }
  }

  // if reactive objects like ref() have been passed in, use watchEffect
  // to watch and rerun the dragSelect init if they change

  if (isRef(selectables) || isRef(area)) {
    watchEffect(initDragSelect)
  }

  function refreshSelected () {
    if (dragSelectInstance) {
      const dsSelection = dragSelectInstance.getSelection()
      selected.value = dsSelection
    }
  }

  // a composable can also hook into its owner component's
  // lifecycle to setup and teardown side effects.
  onMounted(() => {
    initDragSelect()
    if (dragSelectInstance) {
      dragSelectInstance.subscribe('callback', (callbackObject) => {
        refreshSelected()
      })
    }
  })
  onUnmounted(() => {
    if (dragSelectInstance) dragSelectInstance.stop()
  })

  // expose managed state as return value
  return selected
}
ThibaultJanBeyer commented 1 year ago

Thanks a lot!! If others can confirm that this is the proper way, I’ll add it to the documentation. As mentioned I’ve no clue myself and would need to learn Vue first to be able to give feedback on it.

In the long run, ideally, my plan is to have DragSelect wrappers for each JavaScript framework in the framework hell 😅

image

But that will take a while… Unless people knowledgeable in the specific framework are willing to help :)

justinross commented 1 year ago

I've made some updates that were helpful in my use. Mostly, returning more than just the list of selected elements, updating the selected elements on dragmove instead of callback, and improved handling of Vue components being sent in as selectables and selection area (the last uses unrefElement from vueuse. There's probably a way to do this without it, but I don't know what it is, and this works well. I'd also love for someone that knows Vue better than I do to take a look at it, though. :)

import { unref, isRef, ref, onMounted, onUnmounted, watchEffect } from 'vue';
import { unrefElement } from '@vueuse/core';
import DragSelect from 'dragselect';

export function useDragSelect(selectables, area) {
  // state encapsulated and managed by the composable
  const selected = ref([]);
  let dragSelectInstance: DragSelect;

  function initDragSelect() {
    // This is all doing some checking/cleanup to allow for different types of input
    // selectables is always an array, but it can be an array of HTML elements,
    // a vue ref() of HTML elements, or an array of Vue components
    // Similarly, area can be either an HTML element or Vue component, ref() or not

    if (unref(selectables) && unref(area)) {
      const selectableElementArray = unref(selectables).map((el) => {
        return unrefElement(el);
      });

      const areaElement = unrefElement(area);

      if (dragSelectInstance instanceof DragSelect) {
        dragSelectInstance.stop();
        dragSelectInstance.setSettings({
          area: areaElement,
          draggability: false,
        });
        dragSelectInstance.removeSelectables(
          dragSelectInstance.getSelectables(),
          true,
          true
        );
        dragSelectInstance.addSelectables(selectableElementArray);
        dragSelectInstance.start();
      } else {
        dragSelectInstance = new DragSelect({
          area: areaElement,
          selectables: selectableElementArray,
          draggability: false,
        });
        dragSelectInstance.subscribe('dragmove', () => {
          refreshSelected();
        });
      }
    }
  }

  // if reactive objects like ref() have been passed in, use watchEffect
  // to watch and rerun the dragSelect init if they change

  if (isRef(selectables) || isRef(area)) {
    watchEffect(initDragSelect);
  }
  function getInstance() {
    return dragSelectInstance;
  }

  function clearSelected() {
    dragSelectInstance.clearSelection(true);
  }

  function refreshSelected() {
    if (dragSelectInstance) {
      const dsSelection = dragSelectInstance.getSelection();
      selected.value = dsSelection;
    }
  }

  // a composable can also hook into its owner component's
  // lifecycle to setup and teardown side effects.
  onMounted(() => {
    initDragSelect();
  });
  onUnmounted(() => {
    if (dragSelectInstance) dragSelectInstance.stop();
  });

  // return composable's managed state as return value
  return { selected, clearSelected, instance: getInstance() };
}