bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
37.22k stars 1.26k forks source link

How could you implement a nested SortableJS with HTMX? #1192

Closed helixaros closed 1 year ago

1cg commented 1 year ago

I would catch the end event on each sortable w/ some scripting and trigger an event specific to that list that you could listen for w/hx-trigger.

Easiest solution I can think of.

jjdechavez commented 1 year ago

Hello @1cg, I do have a question about having 2 SortableJS shared list that are grouped. When I dragged an item to other shared list, I want to trigger the form submission where the item has been dragged. Currently, I'm experiencing it only trigger the form submission where the item removed/moved on parent/belongs list. Is there any way that I could achieve those goals. Thank you very much!

@each(section in sections)
    <form
      hx-post="{{ route('workspaces.tasks.position', { id: workspace.id, sectionId: section.id }) }}"
      hx-trigger="end"
      hx-target="#section-{{ section.id }}"
      hx-swap="outerHTML"
    >
      {{ csrfField() }}
      <ul id="section-{{ section.id }}" class='sortable sections'>
        @each(task in section.tasks)
          <li>
            <div>
              <input type="hidden" name="taskIds" value="{{ task.id }}">
              {{ task.priority }}
            </div>
            <div>
              {{ task.title }} - {{ task.position }}
            </div>
            <div>
              {{ task.createdAt.toFormat('LLL dd yyyy') }}
            </div>
          </li>
        @end
      </ul>
    </form>
@end

javascript

import Sortable from 'sortablejs';
import htmx from 'htmx.org';

htmx.onLoad(function () {
  const workspaceTasks = document.querySelectorAll('.sortable');

  for (var i = 0; i < workspaceTasks.length; i++) {
    let sortable = workspaceTasks[i];
    new Sortable(sortable, {
      group: 'sections',
      filter: '.task-option',
      animation: 150,
    });
    htmx.process(sortable);
  }
});
jjdechavez commented 1 year ago

I did some clever workaround by calling an ajax call with onEnd method of SortableJS, but it still has bug like if the hx-trigger="end" resolve first and the SortableJS onMethod ajax call resolve last. It will return some issue like you can see the dragged item from the previous list. I'm still trying to figure out if there's better way to resolve this kind issue.

import Sortable from 'sortablejs';

// htmx is already on window
htmx.onLoad(function () {
  const workspaceTasks = document.querySelectorAll('.sortable');

  for (var i = 0; i < workspaceTasks.length; i++) {
    let sortable = workspaceTasks[i];
    new Sortable(sortable, {
      group: 'sections',
      filter: '.task-option',
      animation: 150,
      onEnd: (evt) => {
        if (evt.pullMode) {
          const toElement = evt.to;
          const childrens = Array.from(toElement.children);
          const ids = childrens.map(
            (child) => child.querySelector('input').value
          );
          const formElement = toElement.closest('form');
          const csrfToken = formElement
            .querySelector("input[name='_csrf']")
            .attributes.getNamedItem('value').value;

          const htmxPath = formElement.attributes
            .getNamedItem('hx-post')
            .value.replace('position', 'drag');

          const htmxTarget =
            formElement.attributes.getNamedItem('hx-target').value;
          const htmxSwap = formElement.attributes.getNamedItem('hx-swap').value;
          const htmxValues = { taskIds: ids };

          htmx.ajax('POST', htmxPath, {
            target: htmxTarget,
            swap: htmxSwap,
            values: htmxValues,
            headers: {
              'x-csrf-token': csrfToken,
            },
          });
        }
      },
    });

    htmx.process(sortable);
  }
});
jjdechavez commented 1 year ago

Already solved it! What I did was call ajax on onEnd method of SortableJS if the event params from onEnd is evt.pullMode you can put your logic on how you're going to transfer the item into other list and persist on your backend and fetch the updated items and attach it on your partials html.

I hope it helps :)

Sample:

import Sortable from 'sortablejs';

// htmx is already on window
htmx.onLoad(function () {
  const workspaceTasks = document.querySelectorAll('.sortable');

  for (var i = 0; i < workspaceTasks.length; i++) {
    let sortable = workspaceTasks[i];
    new Sortable(sortable, {
      group: 'sections',
      filter: '.task-option',
      animation: 150,
      onEnd: (evt) => {
        if (evt.pullMode) {
          // Dragged item to other list
         // ... your logic

          htmx.ajax('POST', `/dashboard/workspaces/${workspaceId}/drag`, {
            target: '#section-list',
            swap: 'outerHTML',
            values: {
              sections: JSON.stringify({
                [toElementId]: toIds,
                [fromElementId]: fromIds,
              }),
            },
            headers: {
              'x-csrf-token': csrfToken,
            },
          });

          return true;
        }

       // Changing the item position
       // .... your logic

        htmx.ajax(
          'POST',
          `/dashboard/workspaces/${workspaceId}/sections/${workspaceSectionId}/position`,
          {
            target: '#section-list',
            swap: 'outerHTML',
            values: {
              taskIds: toElementIds,
            },
            headers: {
              'x-csrf-token': csrfToken,
            },
          }
        );
      },
    });

    htmx.process(sortable);
  }
});