sortable icon indicating copy to clipboard operation
sortable copied to clipboard

Is it possible to use (nested) Livewire components with Livewire Sortable?

Open Aquive opened this issue 3 years ago • 1 comments

Question like in the title. Is it possible to use (nested) Livewire components with Livewire Sortable?

I am working on a project which have boards, which has groupsm which has tasks. Boards, group and tasks are Livewire componets. When I try to implement Sortable I on groups I get onwanted/weird results.

Is it even possible to combine it with Livewire components?

  • And if so, what is the best way to implement?
  • Should wire:sortable.item be on the component element which is being looped? Or should it be inside the component view template?
  • I searched trough the issues and I saw more topics. How sould the key and wire:key tags be used? And where should they be placed? Inside the component view template or in the parent?

Any help to point me in the right direction is appreciated.

Edit: I am also using https://github.com/livewire-ui/modal. It seems to me when combining all of those things (livewire, livewire Sortable and Livewire UI modal) things get too complicated. I don't know maybe I am doing something wrong.

Aquive avatar Oct 22 '21 11:10 Aquive

Hi, I had a similar use case, I needed to sort parent components, and inside the parent components there are multiple sortables aswell. I ended up modifying the source code of this package. Maybe I'll do some refactoring and add a PR. All you need to do is (instead of importing this package) adding the following modified JS, the shopify/draggable package and make a few modifications on your markup. Hope it helps someone:

npm i @shopify/draggable@next

Add wire:sortable.key="your-unique-key-per-sortable" to every wire:sortable, wire:sortable.item and wire:sortable.handle, e.g:

<ul wire:sortable="updateDisplayOrder" wire:sortable.key="sk-1">
@foreach($items as $key => $item)
   <li wire:key="item-{{$key}}"
        class="list-group-item"
        wire:sortable.item="{{ $key }}"
        wire:sortable.key="sk-1">
        ...
   </li>
@endforeach
</ul>

Add this JS somewhere on your page. This extends the "sortable" directive to look for the wire:sortable.key property.


import Sortable from '@shopify/draggable/lib/sortable';

if (typeof window.livewire === 'undefined') {
    throw 'Livewire Sortable Plugin: window.livewire is undefined. Make sure @livewireScripts is placed above this script include'
}

window.livewire.directive('sortable-group', (el, directive, component) => {
    if (directive.modifiers.includes('item-group')) {
        // This will take care of new items added from Livewire during runtime.
        el.closest('[wire\\:sortable-group]').livewire_sortable.addContainer(el)
    }

    // Only fire the rest of this handler on the "root" directive.
    if (directive.modifiers.length > 0) return

    let options = {
        draggable: '[wire\\:sortable-group\\.item]',
        mirror: {
            constrainDimensions: true,
        },
    }


    if (el.querySelector('[wire\\:sortable-group\\.handle]')) {
        options.handle = '[wire\\:sortable-group\\.handle]'
    }

    const sortable = el.livewire_sortable = new Sortable([], options);

    sortable.on('sortable:stop', () => {
        setTimeout(() => {
            let groups = []

            el.querySelectorAll('[wire\\:sortable-group\\.item-group]').forEach((el, index) => {
                let items = []
                el.querySelectorAll('[wire\\:sortable-group\\.item]').forEach((el, index) => {
                    items.push({order: index + 1, value: el.getAttribute('wire:sortable-group.item')})
                })

                groups.push({
                    order: index + 1,
                    value: el.getAttribute('wire:sortable-group.item-group'),
                    items: items,
                })
            })

            component.call(directive.method, groups)
        }, 1)
    })
})

window.livewire.directive('sortable', (el, directive, component) => {
    // Only fire this handler on the "root" directive.
    if (directive.modifiers.length > 0) return

    let key = el.getAttribute('wire:sortable.key');
    let draggable = '[wire\\:sortable\\.item]';
    let handle = '[wire\\:sortable\\.handle]';

    if (key) {
        let keyAttr = '[wire\\:sortable\\.key="' + key + '"]';
        draggable += keyAttr;
        handle += keyAttr;
    }

    let options = {
        draggable: draggable,
        mirror: {
            constrainDimensions: true,
        }
    }

    if (el.querySelector(handle)) {
        options.handle = handle;
    }

    const sortable = new Sortable(el, options);

    sortable.on('sortable:stop', () => {
        setTimeout(() => {
            let items = []

            el.querySelectorAll(draggable).forEach((el, index) => {
                items.push({order: index + 1, value: el.getAttribute('wire:sortable.item')})
            })

            component.call(directive.method, items)
        }, 1)
    })
})

Edit: You need wire:key on the top level element inside a @foreach. I usually also put a key on child components so they rerender properly: <livewire:my-component :key="$something->id . time()" />

MineChamp2000 avatar Jan 04 '22 13:01 MineChamp2000