A Stimulus (JS) “List” Controller

Dylan Phan
2 min readNov 19, 2020

A dead simple Stimulus controller for adding/removing/reordering a list of items in HTML. I’m using this in a Ruby on Rails form for inputting a list of survey questions. This post shows you how to handle the array on the Rails end.

Here’s what the controller expects:

actions:

  • up /down/remove— put these on the buttons that will reorder the items or remove items.
  • add — put this on the button that will add a new item

data:

  • data-list-list-item-template-id — the id of your <template>. Upon add, the HTML inside the template will be copied to create your new list item.

targets:

  • up/down/remove—put these on the buttons that will reorder the items or remove items. The target attribute must be given to the same element as the up/down/remove action attributes.
  • listItem — the wrapper for your list item. This is the element that gets moved around.
  • container — the direct parent of the listItems. container must contain no other children. (To get rid of this requirement, you can modify the controller to use this method)

To use it, look at the fiddle below and copy the application.register portion of the code into your app. In Ruby on Rails (6), it would be in app/javascript/packs/application.js. Check out the HTML too for an example of how to use the controller. The fiddle is a working demo you can play with.

Here’s the javascript in case JSFiddle goes down:

const application = Stimulus.Application.start()application.register(“list”, class extends Stimulus.Controller {
static get targets() {
return [“up”, “down”, “remove”, “listItem”, “container”]
}
up(event) {
const index = this.upTargets.findIndex(t => t === event.target);
const listItemElement = this.listItemTargets[index];
listItemElement.previousElementSibling.insertAdjacentElement(‘beforebegin’, listItemElement);
}
down(event) {
const index = this.downTargets.findIndex(t => t === event.target);
const listItemElement = this.listItemTargets[index];
const nextListItemElement = listItemElement.nextElementSibling;
nextListItemElement.insertAdjacentElement(‘afterend’, listItemElement);
}

remove(event) {
const index = this.removeTargets.findIndex(t => t === event.target);
const listItemElement = this.listItemTargets[index];
listItemElement.remove();
}
add(event) {
const templateId = this.data.get(“listItemTemplateId”);
const template = document.getElementById(templateId);
this.containerTarget.appendChild(template.content.cloneNode(true));
}
});

--

--