Skip to content

Instantly share code, notes, and snippets.

@jaredcwhite
Last active April 21, 2024 14:28
Show Gist options
  • Save jaredcwhite/196f97cafeeaf8e2d5a82c6e9d79a069 to your computer and use it in GitHub Desktop.
Save jaredcwhite/196f97cafeeaf8e2d5a82c6e9d79a069 to your computer and use it in GitHub Desktop.
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()
    }
  }
};

@laptopmutia
Copy link

are these codes really necessary at the current moment?

I really feel intimidated by the boiler code, because on thoughts, shoelace should make things simpler

@KonnorRogers
Copy link

@joyoy96 this is old and from when Shoelace didn't work with regular forms

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