Hotwire Discussion

Adding specific code to generic controllers

Hi, I have implemented a controller which instantiates drag and drop functionality using the Sortable library, and I want to know how I can keep the controller generic whilst defining what should happen in the onEnd event for a specific use case.

Here is the code:

import { Controller } from "stimulus";
import Sortable from "sortablejs";

export default class extends Controller {
  connect() {
    this.sortable = Sortable.create(this.element, {
      onEnd: this.onEnd
    })
  }

  onEnd() {
    // This code will depend on the context, how should it be arranged?
  }
}

Thanks

Can you provide some specific examples of what customizations you might want in onEnd?

Well the piece I’m currently working on is around allowing a user to specify the order in which something appears. So there is a position attribute on the model and collections are ordered by it.

So in this scenario the user is dragging collection items into their specified order. Once they release the draggable element, I want to iterate over the hidden position fields that each of them has and set them to 1, 2, 3, 4 etc. The code is something like:

$(".sortable-list-item").each(function(i, listItem) {
  $(listItem).find(".signatory-fields-position").val(i + 1);
});

But of course this is just one example of what I might want to use drag and drop for.

One common pattern is to use a MutationObserver (see Dealing with sorted data in a stream · Issue #109 · hotwired/turbo · GitHub).

// application_controller.js
import { Controller } from 'stimulus';

export default class extends Controller {
  observeMutations(callback, target = this.element, options = { childList: true, subtree: true }) {
    const observer = new MutationObserver(mutations => {
      observer.disconnect()
      Promise.resolve().then(start)
      callback.call(this, mutations)
    })
    function start() {
      if (target.isConnected) observer.observe(target, options)
    }
    start()
  }
}

// sortable_controller.js
import { Controller } from 'stimulus';
import Sortable from "sortablejs";

export default class extends Controller {
  static values = { options: Object }

  connect() {
    const options = this.hasOptionsValue ? this.optionsValue : {};
    this.sortable = Sortable.create(this.element, options)
  }

  disconnect() {
    this.sortable.destroy();
  }
}

// order_input_controller.js
import { ApplicationController } from "./application_controller";

export default class extends ApplicationController {
  initialize() {
    this.observeMutations(this.saveChildrenOrder)
  }

  saveChildrenOrder() {
    this.inputTargets.forEach((target, index) => target.value = index + 1)
  }
}

There’s a lot going on there that I don’t understand, so will have to take the time to properly absorb it when I get the chance. Thanks for taking the time to reply.