One of the coolest additions to rails and turbo is the turbo modals. Just configure the following:
lang-js // app/javascript/application.js Turbo.setConfirmMethod((message, element) => { return new Promise((resolve) => { const modal = new bootstrap.Modal(document.getElementById("turbo-confirm")) const messageElement = document.getElementById("turbo-confirm-message") messageElement.textContent = message modal.show() const modalElement = modal._element const handleConfirm = (event) => { if (event.target.closest(".modal-footer button")) { resolve(event.target.value === "confirm") } } const handleHide = () => { modalElement.removeEventListener("click", handleConfirm) modalElement.removeEventListener("hidden.bs.modal", handleHide) resolve(false) } modalElement.addEventListener("click", handleConfirm) modalElement.addEventListener("hidden.bs.modal", handleHide) }) })
Then slap a bootstrap modal into `application.html.erb`
lang-erb <!-- app/views/layouts/application.html.erb --> <div class="modal fade" id="turbo-confirm" tabindex="-1" aria-labelledby="turbo-confirm-label" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="turbo-confirm-label">Confirmation required</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <p id="turbo-confirm-message"></p> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" value="cancel">Cancel</button> <button type="button" class="btn btn-primary" data-bs-dismiss="modal" value="confirm">Confirm</button> </div> </div> </div> </div>
Every time we use data-turbo-submits-with on a form or a button_to, it will use this as a confirmation modal. That's great, but what if we wanted to be able to use `window.confirm` the same way?
It is possible, and we can do almost the same thing:
It is possible, and we can do almost the same thing:
lang-js window.showConfirmModal = function showConfirmModal(message) { return new Promise((resolve) => { const modal = new bootstrap.Modal(document.getElementById("turbo-confirm")) const messageElement = document.getElementById("turbo-confirm-message") messageElement.textContent = message modal.show() const modalElement = modal._element const handleConfirm = (event) => { if (event.target.closest(".modal-footer button")) { resolve(event.target.value === "confirm") } } const handleHide = () => { modalElement.removeEventListener("click", handleConfirm) modalElement.removeEventListener("hidden.bs.modal", handleHide) resolve(false) } modalElement.addEventListener("click", handleConfirm) modalElement.addEventListener("hidden.bs.modal", handleHide) }) } ;["turbo:load", "turbo:render"].forEach((event) => { document.addEventListener(event, () => { document.addEventListener( "confirm", (event) => { const element = event.target.closest("[data-confirm]") if (element) { event.preventDefault() event.stopPropagation() showConfirmModal(element.dataset.confirm).then((confirmed) => { if (confirmed) { element.dataset.confirm = "" element.dispatchEvent( new Event("click", { bubbles: true, cancelable: true }), ) } }) } }, true, ) }) })
This is great but we will run into problems with capybara if we are testing with accept_confirm. The problem is that the browser won't pop up at the top level so capybara might not find the modal depending on what your current scope is. Take the following for example:
lang-ruby sell_order_tab("Trade") within "#sell_order_trade_tab" do # Click the toggle button find(".show_car_info_toggle").click # Check for the specific content within ".show_car_info .show_car" do expect(page).to have_content("MYR777") end accept_confirm("If you delete the trade, remember to check the calculations.") do click_on "Delete trade" end end
This will now fail with Capybara::ModalNotFound.
The solution is to create some helper methods to help us match the modal at the document level:
The solution is to create some helper methods to help us match the modal at the document level:
lang-ruby def accept_custom_confirm(message = nil) yield handle_confirm(message:, button: "Confirm") end def dismiss_custom_confirm(message = nil) yield handle_confirm(message:, button: "Cancel") end def handle_confirm(message: nil, button: "Confirm") modal = page.document.find_by_id("turbo-confirm", visible: true) expect(modal).to have_content(message) if message modal.click_on(button) end
All that is required now is to change from accept_confirm to accept_custom_confirm.
accept_custom_confirm("If you delete the trade, remember to check the calculations.") do click_on "Delete trade" end