Skip to content

Instantly share code, notes, and snippets.

@jaredcwhite
Last active July 19, 2021 18:47
Embed
What would you like to do?
Use Shoelace Form Components with Hotwire Turbo

This is a Stimulus controller which will allow a Shoelace Form component to be processed and submitted by Turbo.

Just wrap your <sl-form> control with a form tag:

<%= form_with url: "/test-submit" do %>
  <sl-form data-controller="shoelace-form">
    controls go here
  </sl-form>
<% end %>

This gist includes a Ruby variant and an ES variant, because I wrote the controller using Ruby2JS. You can use the ES variant if you don't have Ruby2JS (it is the direct transpiled output from Ruby2JS).

If you're using Ruby2JS + the Webpack loader, you can just drop the Ruby controller into your project under app/javascript/controllers. Make sure you have the Stimulus autoimport configured in your rb2js.config.rb file:

    def self.options
      {
        eslevel: 2021,
        include: :class,
        underscored_private: true,
        autoimports: {
          [:Controller] => "stimulus",
        }
      }
    end

Also make sure you've updated your Stimulus controller context to load Ruby2JS files in app/javascript/controllers/index.js:

const context = require.context("controllers", true, /_controller\.js(\.rb)?$/)
import { Controller } from "stimulus"
class ShoelaceFormController extends Controller {
connect() {
this.element.addEventListener(
"sl-submit",
this.submitForm.bind(this)
);
this.element.closest("form").addEventListener(
"turbo:submit-end",
this.submitEnd.bind(this)
)
};
submitForm(e) {
let form = this.element.closest("form");
let submitter = this.element.querySelector("sl-button[submit]");
if (submitter.disabled) return;
submitter.loading = true;
submitter.disabled = true;
for (let entry of e.detail.formData) {
let [k, v] = entry;
let el = document.createElement("input");
el.type = "hidden";
el.name = k;
el.value = v;
el.deleteMeLater = true;
form.append(el)
};
let el = document.createElement("input");
el.name = "commit";
el.type = "submit";
el.value = submitter.innerHTML;
el.style.display = "none";
el.deleteMeLater = true;
form.append(el);
el.click()
};
submitEnd(e) {
let submitter = e.target.querySelector("sl-button[submit]");
if (submitter) {
submitter.loading = false;
submitter.disabled = false;
this.reset(e.target)
}
};
reset(form) {
form.querySelector("sl-form").getFormControls().then((controls) => {
for (let control of controls) {
switch (control.tagName.toLowerCase()) {
case "sl-checkbox":
case "sl-radio":
control.checked = false;
break;
default:
control.value = ""
}
}
});
for (let control of form.querySelectorAll("input")) {
if (control.deleteMeLater) control.remove()
}
}
};
export default ShoelaceFormController
class ShoelaceFormController < Controller
def connect()
self.element.add_event_listener("sl-submit", submit_form.bind(self))
self.element.closest("form").add_event_listener(
"turbo:submit-end", submit_end.bind(self)
)
end
def submit_form(e)
form = self.element.closest("form")
submitter = self.element.query_selector("sl-button[submit]")
return if submitter.disabled? # form already submitted, not submitting twice
submitter.loading = true
submitter.disabled = true
e.detail.form_data.each do |entry|
k, v = entry
el = document.create_element("input")
el.type = "hidden"
el.name = k
el.value = v
el.delete_me_later = true
form.append(el)
end
el = document.create_element("input")
el.name = "commit"
el.type = "submit"
el.value = submitter.inner_html
el.style.display = "none"
el.delete_me_later = true
form.append(el)
el.click()
end
def submit_end(e)
submitter = e.target.query_selector("sl-button[submit]")
if submitter
submitter.loading = false
submitter.disabled = false
reset(e.target)
end
end
def reset(form)
form.query_selector("sl-form").get_form_controls().then do |controls|
controls.each do |control|
case control.tag_name.downcase()
when "sl-checkbox", "sl-radio"
control.checked = false
else
control.value = ""
end
end
end
form.query_selector_all("input").each do |control|
control.remove() if control.delete_me_later?
end
end
end
export default ShoelaceFormController
@KonnorRogers
Copy link

KonnorRogers commented Apr 16, 2021

Typescript version:

import { Controller } from "stimulus"
import SlForm from '@shoelace-style/shoelace/dist/components/form/form'
import SlButton from '@shoelace-style/shoelace/dist/components/button/button'

export default class ShoelaceFormController extends Controller {
  element!: HTMLElement;
  
  connect(): void {
    this.element.addEventListener(
      "sl-submit",
      this.submitForm.bind(this) as EventListener
    )

    this.element?.closest("form")?.addEventListener(
      "turbo:submit-end",
      this.submitEnd.bind(this) as EventListener
    )
  };

  submitForm(e: CustomEvent): void {
    let form = this.element.closest("form");
    if (!form) return;

    let submitter = this.element.querySelector("sl-button[submit]") as SlButton;
    if (submitter.disabled) return;
    submitter.loading = true;
    submitter.disabled = true;

    for (let entry of e.detail.formData) {
      let [k, v] = entry;
      let el = document.createElement("input");
      el.type = "hidden";
      el.name = k;
      el.value = v;
      el.dataset.deleteMeLater = "true";
      form.append(el)
    };

    let el = document.createElement("input");
    el.name = "commit";
    el.type = "submit";
    el.value = submitter.innerHTML;
    el.style.display = "none";
    el.dataset.deleteMeLater = "true";
    form.append(el);
    el.click()
  };

  submitEnd(e: Event): void {
    const target = e.target as SlForm;
    let submitter = target.querySelector("sl-button[submit]") as SlButton;

    if (submitter) {
      submitter.loading = false;
      submitter.disabled = false;
      this.reset(target)
    }
  };

  reset(form: SlForm) {
    const slForm = form.querySelector("sl-form") as SlForm
    const formControls = slForm.getFormControls() as HTMLInputElement[]

    for (let control of formControls) {
      switch (control.tagName.toLowerCase()) {
      case "sl-checkbox":
      case "sl-radio":
        control.checked = false;
        break;

      default:
        control.value = ""
      }
    }

    for (let control of form.querySelectorAll("input")) {
      if (control.dataset.deleteMeLater === "true") control.remove()
    }
  }
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment