{"version":"https://jsonfeed.org/version/1.1","title":"OnRailsBlog","description":"Ruby on Rails, Stimulus, Hotwire, and Turbo tutorials","home_page_url":"https://onrails.blog","feed_url":"https://onrails.blog/blog/feed.json","language":"en","items":[{"id":"https://onrails.blog/2024/06/01/interactive-modals","url":"https://onrails.blog/2024/06/01/interactive-modals","title":"Interactive Modals","date_published":"2024-06-01T14:58:15Z","date_modified":"2026-06-02T00:41:23Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThere is a lot of talk about modals in Rails, and this tutorial shows you how with some Stimulus sprinkles, you can have very interactive modals.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/VKJ_1qoPTsE\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-4-3 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/VKJ_1qoPTsE\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe code for the demo can be found on Github: https://github.com/OnRailsBlog/interactive_modal\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":3} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/05/08/headless-ui-with-stimulusjs-and-an-outlet-screencast","url":"https://onrails.blog/2024/05/08/headless-ui-with-stimulusjs-and-an-outlet-screencast","title":"Headless UI with StimulusJS and an Outlet","date_published":"2024-05-08T15:09:37Z","date_modified":"2026-06-02T00:41:21Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHeadless UI 2.0 just came out from TailwindLabs. I often find myself using their components in my projects, and I wanted to show you my process for converting their React components to use Stimulus, since I haven’t been using React in any of my projects.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/fvm0F_f5oBA\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-4-3 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/fvm0F_f5oBA\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eSwitching on Interactivity\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe component I wanted to use was the \u003ca href=\"https://headlessui.com/react/switch\"\u003eSwitch\u003c/a\u003e. It’s styled after the native iOS switch toggle, where you turn an option on or off. Tapping anywhere on the button will transition the inner circle from one side to the other. The version of this switch uses CSS animations, and all our controller would need to do is update a \u003ccode\u003edata-checked\u003c/code\u003e attribute on the button, and CSS will animate the sliding action. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, I copied the HTML from the rendered React component, and you get:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\u0026lt;button \n    class=\"group relative flex h-7 w-14 cursor-pointer rounded-full bg-blue-900/10 p-1 transition-colors duration-200 ease-in-out focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white data-[checked]:bg-blue-900/30\" \n    role=\"switch\" \n    type=\"button\" \n    tabindex=\"0\" \n    aria-checked=\"true\"\n\tdata-checked\u0026gt;\n    \u0026lt;span aria-hidden=\"true\" class=\"pointer-events-none inline-block size-5 translate-x-0 rounded-full bg-white ring-0 shadow-lg transition duration-200 ease-in-out group-data-[checked]:translate-x-7\"\u0026gt;\u0026lt;/span\u0026gt;\n  \u0026lt;/button\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLet’s generate a Stimulus controller called \u003ccode\u003eswitch\u003c/code\u003e that will handle the clicks for us:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e$ ./bin/rails g stimulus switch \n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAdd the controller and an action to the \u003ccode\u003e\u0026lt;button\u0026gt;\u003c/code\u003e tag:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003edata-controller=\"switch\"\ndata-action=\"switch#toggle\"\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen when the button is clicked, it will fire the toggle action on the controller:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  toggle() {\n    if (this.element.dataset.checked) {\n      delete this.element.dataset.checked;\n      this.element.ariaChecked = false;\n    } else {\n      this.element.dataset.checked = true;\n      this.element.ariaChecked = true;\n    }\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow when you click the button, the switch toggles back and forth. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAdding an Outlet for the Switch\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:image {\"id\":1142,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image size-full\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA3NiwicHVyIjoiYmxvYl9pZCJ9fQ==--4e6b2dbcc91c849f185d4dd82ba70add4925f059/Headless-UI-Switch-Outlet.gif\" alt=\"\" class=\"wp-image-1142\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis switch just exists on its own right now. It would be great if it could actually update a value on a form. We can fix this with \u003ca href=\"https://stimulus.hotwired.dev/reference/outlets\"\u003eStimulus Outlets\u003c/a\u003e. This ability allows our controller to talk to another controller on the page. Stimulus uses the controller name and a CSS path to connect to the correct element on the page. We start by adding the input checkbox that we want to access: \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\u0026lt;input type=\"checkbox\" id=\"todo-complete\" data-controller=\"input\"\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen we add a data attribute to the \u003ccode\u003eswitch\u003c/code\u003e controller:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003edata-switch-input-outlet=\"#todo-complete\"\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the Stimulus controller, we add the outlet declaration:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003estatic outlets = [\"input\"];\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, we can access the \u003ccode\u003einputOutletElement\u003c/code\u003e in the \u003ccode\u003eswitch\u003c/code\u003e controller, and set it to checked, and read its value. You can update the \u003ccode\u003etoggle()\u003c/code\u003e method:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  toggle() {\n    this.inputOutletElement.checked = !this.inputOutletElement.checked;\n    if (this.inputOutletElement.checked) {\n      this.element.dataset.checked = true;\n      this.element.ariaChecked = true;\n    } else {\n      delete this.element.dataset.checked;\n      this.element.ariaChecked = false;\n    }\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eIn Closing\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHeadless UI is great for some prebuilt components using Tailwind CSS. Even though they’re built in React, we can use Stimulus to add some interactivity. Stimulus Outlets allow us to target elements on the page outside the hierarchy of the controller. I can’t wait to use this more in my applications.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/04/23/animate-filtering-data-in-hotwire","url":"https://onrails.blog/2024/04/23/animate-filtering-data-in-hotwire","title":"Animate Filtering Data in HOTWire","date_published":"2024-04-23T17:30:18Z","date_modified":"2026-06-02T00:41:21Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe can progressively enhance the filtering search. In the \u003ca title=\"https://onrails.blog/2024/04/09/filtering-data-in-hotwire/(opens in a new tab)\" href=\"https://onrails.blog/2024/04/09/filtering-data-in-hotwire/\"\u003eprevious tutorial\u003c/a\u003e, we saw how easy it is to use Turbo 8’s morphing and a very simple Stimulus controller to trigger the request back to the server. We can dig deeper into the events and get a very satisfying animation of Todos disappearing when they’re filtered out, and reappearing when they are back in the list.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/cDZ1gTEnABI\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-16-9 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/cDZ1gTEnABI\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eListening for Morphing\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOur Stimulus controller is going to get some new methods. It will hook into the Turbo event stream. The first change is to add the \u003ccode\u003ereplace\u003c/code\u003e option to the Turbo visit in the controller. This will effectively make Turbo morph the new changes on the page \u003csup\u003e\u003ca id=\"ffn1\" href=\"#fn1\" class=\"footnote\"\u003e1\u003c/a\u003e\u003c/sup\u003e. We want morphing, because we can listen for the upcoming changes, and tweak the behavior for elements that are changing. This will fire the morphing events.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAnimating removals\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eRemoving items is easier, since we get the items that are going to be removed from the page, and we tell Turbo \u003cem\u003enot to remove them\u003c/em\u003e. Now, we handle the removal, after a pleasing animation.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAdd a new action to the Search controller: \u003ccode\u003eturbo:before-morph-element@document-\u0026gt;search#animateRemovals\u003c/code\u003e. This will be called every time an element inside the page is going to change, so I added a data attribute to the Todo items to flag that they should be animated away: \u003ccode\u003edata-animate-exit=\"true\" \u003c/code\u003e. The \u003ccode\u003eanimateRemovals\u003c/code\u003e method in the Search controller will check that this property exists.  \u003ca href=\"https://turbo.hotwired.dev/reference/events#turbo%3Abefore-morph-element\"\u003e\u003ccode\u003eturbo:morph-element\u003c/code\u003e\u003c/a\u003e will have a property called \u003ccode\u003enewElement\u003c/code\u003e if the element getting replaced. Since the Todos are getting filtered, and will no longer be on the page, the \u003ccode\u003enewElement\u003c/code\u003e will be null. Once we have the element to animate away, calling \u003ccode\u003epreventDefault()\u003c/code\u003e on the event will cancel the element form being removed. Our controller takes over the responsibility for removing the element by calling a \u003ccode\u003eanimateExit()\u003c/code\u003e function that can be tweaked for the preferred animation style.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\n  animateRemovals(event) {\n    if (event.detail.newElement == null \u0026amp;\u0026amp; event.target.dataset.animateExit) {\n      event.preventDefault();\n      this.animateExit(event.target);\n    }\n  } \n\n  async animateExit(target) {\n    target.animate(\n      [\n        {\n          transform: `scale(1, 1)`,\n          transformOrigin: \"center\",\n          height: \"auto\",\n          opacity: 1.0,\n        },\n        {\n          transform: `scale(0.9, 0.7)`,\n          opacity: 0.2,\n          height: \"80%\",\n          offset: 0.8,\n        },\n        {\n          transform: `scaleY(0.8, 0)`,\n          transformOrigin: \"center\",\n          height: 0,\n          opacity: 0,\n        },\n      ],\n      {\n        duration: 75,\n        easing: \"ease-out\",\n      }\n    );\n    await Promise.all(\n      target.getAnimations().map((animation) =\u0026gt; animation.finished)\n    );\n    target.remove();\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe animation in this case uses keyframes to collapse and shrink the Todo as its opacity changes. Play around with what makes sense for your application. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAnimating Insertions\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnimating new elements is trickier. It requires listening for the \u003ca href=\"https://turbo.hotwired.dev/reference/events#turbo%3Amorph-element\"\u003e\u003ccode\u003eturbo:morph-element\u003c/code\u003e\u003c/a\u003e event, which fires after all the changes have been made on the page, but before the page is redrawn. This allows us to make some visual changes to the new Todo elements. The challenge is figuring out which elements need to be animated in. In the case of this event, \u003ccode\u003eevent.details.newElement\u003c/code\u003e refers to the old list, and \u003ccode\u003eevent.target\u003c/code\u003e refers to the list in the dom. First, it goes through the old list to collect all the existing Todo ids. Then it goes through the new list to find ids that are new, and then it animates each of those new Todos.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  animateInsertions(event) {\n    if (\n      event.target.childNodes.length \u0026gt; event.detail.newElement.childNodes.length\n    ) {\n      var existingIds = new Set();\n      for (var i = 0; i \u0026lt; event.detail.newElement.childNodes.length; i++) {\n        let child = event.detail.newElement.childNodes[i];\n        if (\n          child.nodeName != \"#text\" \u0026amp;\u0026amp;\n          child.dataset.animateEntrance \u0026amp;\u0026amp;\n          child.id != null\n        ) {\n          existingIds.add(child.id);\n        }\n      }\n      for (var i = 0; i \u0026lt; event.target.childNodes.length; i++) {\n        let child = event.target.childNodes[i];\n        if (\n          child.nodeName != \"#text\" \u0026amp;\u0026amp;\n          child.dataset.animateEntrance \u0026amp;\u0026amp;\n          child.id != null \u0026amp;\u0026amp;\n          !existingIds.has(child.id)\n        ) {\n          this.animateEntrance(child);\n        }\n      }\n    }\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe animation first sets the height to 0, and shrinks the Todo, and animates the it expanding to fit the size of its original view. Make sure you play around with these different values depending on what you need for your application.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  async animateEntrance(target) {\n    target.animate(\n      [\n        {\n          transform: `scale(0.8, 0.0)`,\n          transformOrigin: \"center\",\n          height: \"0\",\n          opacity: 0.0,\n        },\n        {\n          transform: `scale(0.9, 0.7)`,\n          opacity: 0.2,\n          height: \"80%\",\n          offset: 0.2,\n        },\n        {\n          transform: `scale(1, 1)`,\n          transformOrigin: \"center\",\n          height: \"auto\",\n          opacity: 1,\n        },\n      ],\n      {\n        duration: 100,\n        easing: \"ease-out\",\n      }\n    );\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eFinal enhancement\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAs I was playing with the searching, I realized I don’t necessarily want to hint to the reordering features. I found hiding the buttons was a visual indicator that you may not want to do that. What’s neat is using CSS to hide the buttons when there is text in the search field.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e#search:not(:placeholder-shown) + div {\n  .todo \u0026gt; div \u0026gt; button.up,\n  .todo \u0026gt; div \u0026gt; button.down {\n    animation-duration: .2s;\n    animation-name: fadeOut;\n    animation-timing-function: ease-in-out;\n    animation-fill-mode: forwards;\n  }\n}\n\n@keyframes fadeOut {\n\n  0% {\n    transform: scale(1);\n    transform-origin: center;\n    height: \"auto\";\n    opacity: 1.0;\n  }\n\n  20% {\n    transform: scale(1.2);\n    transform-origin: center;\n    height: \"auto\";\n    opacity: 0.8;\n  }\n\n  100% {\n    transform: scale(0);\n    transform-origin: center;\n    height: 0;\n    opacity: 0;\n    visibility: hidden;\n  }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eInteractive Apps\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIt’s quite possible, and very straightforward to add nice animations as enhancements of our existing applications with just a little Stimulus and the amazing Javascript animations the browsers all support.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/OnRailsBlog/todo_app/tree/5d01750681fed0794d11dd184cb8688a3cdf332f\"\u003eYou can find the source code at this point on Github\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":3} /--\u003e\n\n\u003c!-- wp:list {\"ordered\":true} --\u003e\n\u003col\u003e\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://turbo.hotwired.dev/handbook/drive#application-visits\"\u003ehttps://turbo.hotwired.dev/handbook/drive#application-visits\u003c/a\u003e \u003ca href=\"#ffn1\"\u003e↩\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\u003c/ol\u003e\n\u003c!-- /wp:list --\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/04/09/filtering-data-in-hotwire","url":"https://onrails.blog/2024/04/09/filtering-data-in-hotwire","title":"Filtering Data in HOTWire","date_published":"2024-04-09T14:02:12Z","date_modified":"2026-06-02T00:41:20Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhen we have a long list of Todos, sometimes we want to filter them by name. We can easily do this using Turbo’s morphing and a Stimulus controller to update the page from the server.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/MNKb1Xbu298\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-16-9 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/MNKb1Xbu298\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOne previous way to get this interactivity was to use a Stimulus controller that filtered the HTML. This still works, and might be a strategy depending on your situation. This technique will send the request to the server, and leverage the Database to perform the filtering. This might work better if you have pagination,  or don’t want to load hundreds or thousands of records onto a page to perform filtering.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eFiltering on the Server\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eStart by updating \u003ccode\u003etodos_controller.rb\u003c/code\u003e and the \u003ccode\u003eindex\u003c/code\u003e action. The controller should have default query of the Todos:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e@todos = Todo.all.order(\"priority\")\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe will look for a query parameter called \u003ccode\u003e:todo\u003c/code\u003e that we’ll use to filter the name of the Todo. The action checks for the presence of the parameter, and that it isn’t blank. It lowercases the param, and then performs a lower case query of all the names in the Todo.  If the todo parameter is missing, it pulls in all the Todos. It then sorts the values by priority.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e    query = if params[:todo] \u0026amp;\u0026amp; !params[:todo].nil? \u0026amp;\u0026amp; !params[:todo].blank?\n      name = \"%#{params[:todo].downcase.strip}%\"\n      Todo.where(\"lower(name) like ?\", name)\n    else\n      Todo.all\n    end\n    @todos = query.order(\"priority\")\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eThe Stimulus Search controller\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eYou can generate a new controller we’ll call search:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e$ ./bin/rails g stimulus search\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis controller will have a parameter value for the url to visit, and a single action, \u003ccode\u003esearch\u003c/code\u003e.  When the search action triggers, it will read the name and value from the target that fired the event, append that to the \u003ccode\u003eurlValue\u003c/code\u003e, and tell Turbo to visit the page. Turbo will make the request, and perform the morphing. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003eimport { Controller } from \"@hotwired/stimulus\";\n\n// Connects to data-controller=\"search\"\nexport default class extends Controller {\n  static values = { url: String };\n\n  search(event) {\n    Turbo.visit(`${this.urlValue}?${event.target.name}=${event.target.value}`);\n  }\n}\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eWiring up the HTML\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, we can add an \u003ccode\u003einput\u003c/code\u003e field for our search controller.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  \u0026lt;input \n    id=\"search\" \n    type=\"text\" \n    name=\"todo\" \n    value=\"\u0026lt;%= params[:todo] %\u0026gt;\"\n    placeholder=\"Search\" \n    data-controller=\"search\" \n    data-search-url-value=\"\u0026lt;%= request.path %\u0026gt;\"\n    data-action=\"search#search\"\n    data-turbo-permanent\n    class=\"w-full my-1 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6\"\u0026gt;    \n  \u0026lt;!-- existing todos div --\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe action will call to our Stimulus controller, and fetch the updated results. The form gets set to whatever was passed, so that the page refreshes have the correct information in the search field. The input field is also set as \u003ccode\u003edata-turbo-permanent\u003c/code\u003e so that it keeps focus when the server results come back, and the page is morphed. If it wasn’t there, the text field would lose focus after every keystroke, which provides for a poor user experience.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow you have a simple way to filter data on your page.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":3} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/04/01/hotwire-turbo-tutorial-animated-deletions-and-insertions","url":"https://onrails.blog/2024/04/01/hotwire-turbo-tutorial-animated-deletions-and-insertions","title":"HOTWire \u0026 Turbo Tutorial: Animated Deletions and Insertions","date_published":"2024-04-01T18:54:06Z","date_modified":"2026-06-02T00:41:20Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWith the addition of the new Todo form appearing at the bottom of the Todos, and the delete action removing a Todo, we have a very functional app. It would be nice if those additions and removals had a little animation to emphasize what’s happening on the page. If there was a long list, we might miss the deletion, especially if a network request caused a delay in the removal of the Todo. We can hook into Turbo streams, and run some animations on these actions to make them appear and disappear.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/OY_YraWRXds\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-16-9 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/OY_YraWRXds\n\u003c/div\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eNot animated vs. animated\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAdding a Custom Turbo Action\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTurbo allows for the addition of \u003ca href=\"https://turbo.hotwired.dev/handbook/streams#custom-actions\"\u003ecustom actions\u003c/a\u003e to enhance the experience. Each StreamAction is passed a \u003ca href=\"https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/elements/stream_element.js#L4\"\u003eStreamElement\u003c/a\u003e, which contains the Turbo Frame that was sent from the server. We’re going to add two action which we’ll call \u003ccode\u003eanimated_append\u003c/code\u003e and \u003ccode\u003eanimated_remove\u003c/code\u003e to match the \u003ccode\u003eappend\u003c/code\u003e and \u003ccode\u003eremove\u003c/code\u003e actions. The animated version of the remove action will \u003ca href=\"https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/core/streams/stream_actions.js#L23\"\u003eperform the same removal\u003c/a\u003e, but after some animations run that scale and change the height of the removed element. These animations could be customized depending on the design of the web app. The animated append is a little trickier, but \u003ca href=\"https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/core/streams/stream_actions.js#L9\"\u003eit first adds the elements\u003c/a\u003e, and then immediately changes the height to 0 to height it, and then animates the scaling. Add the actions in \u003ccode\u003eapplication.js\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003eTurbo.StreamActions.animated_remove = async function () {\n  this.targetElements.forEach(async (target) =\u0026gt; {\n    target.animate(\n      [\n        {\n          transform: `scale(1)`,\n          transformOrigin: \"top\",\n          height: \"auto\",\n          opacity: 1.0,\n        },\n        {\n          transform: `scale(0.8)`,\n          opacity: 0.2,\n          height: \"80%\",\n          offset: 0.8,\n        },\n        {\n          transform: `scale(0)`,\n          transformOrigin: \"top\",\n          height: 0,\n          opacity: 0,\n        },\n      ],\n      {\n        duration: 75,\n        easing: \"ease-out\",\n      }\n    );\n    await Promise.all(\n      target.getAnimations().map((animation) =\u0026gt; animation.finished)\n    );\n    target.remove();\n  });\n};\n\nTurbo.StreamActions.animated_append = async function () {\n  this.removeDuplicateTargetChildren();\n  this.targetElements.forEach(async (target) =\u0026gt; {\n    target.append(this.templateElement.content);\n    target.lastElementChild.animate(\n      [\n        {\n          transform: `scaleY(0.0)`,\n          transformOrigin: \"top\",\n          height: \"0\",\n          opacity: 0.0,\n        },\n        {\n          transform: `scale(0.8)`,\n          opacity: 0.2,\n          height: \"80%\",\n          offset: 0.2,\n        },\n        {\n          transform: `scaleY(1)`,\n          transformOrigin: \"top\",\n          height: \"auto\",\n          opacity: 1,\n        },\n      ],\n      {\n        duration: 100,\n        easing: \"ease-out\",\n      }\n    );\n  });\n};\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eSending the frame actions \u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe next bit is to send the frames from the server with those actions. An animated append of the new Todo form can be as simple as:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;turbo-stream action=\"animated_append\" target=\"todos\"\u0026gt;\n  \u0026lt;template\u0026gt;\n\t\u0026lt;%= turbo_frame_tag 'new_todo', target: \"_top\", class: \"flex items-center py-3 pl-3 bg-white border-b pr-11 gap-x-4 todo\" do %\u0026gt;\n    \u0026lt;%= render partial: 'form', locals: { todo: @todo } %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n  \u0026lt;/template\u0026gt;\n\u0026lt;/turbo-stream\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd removing a Todo from the list could be this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;turbo-stream action=\"animated_remove\" target=\"todo_101\"\u0026gt;\n  \u0026lt;template\u0026gt;\u0026lt;/template\u0026gt;\n\u0026lt;/turbo-stream\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003eaction\u003c/code\u003e field on the Turbo Stream needs to correspond to the name of the actions we added in \u003ccode\u003eapplication.js\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAdding helpers in Rails\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe can add custom helpers in our Rails app to make those templates available. Add a helper file in \u003ccode\u003eapp/helpers/\u003c/code\u003e called \u003ccode\u003eturbo_streams_actions_helper.rb\u003c/code\u003e. We’ll append two actions that will mirror the \u003ca href=\"https://github.com/hotwired/turbo-rails/blob/102a491754d46f7dd924201fcfaf879a0f04b11c/app/models/turbo/streams/tag_builder.rb\"\u003e\u003ccode\u003eTagBuilder\u003c/code\u003e in Turbo Rails\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003emodule TurboStreamsActionsHelper\n  module CustomTurboStreamActions\n    def animated_remove(target)\n      action :animated_remove, target, allow_inferred_rendering: false\n    end\n\n    def animated_append(target, content = nil, **, \u0026amp;block)\n      action(:animated_append, target, content, **, \u0026amp;block)\n    end\n  end\n\n  Turbo::Streams::TagBuilder.prepend(CustomTurboStreamActions)\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe can use the actions like any other Turbo Stream action. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003ccode\u003e\u0026lt;%= turbo_stream.append \"todos\" do %\u0026gt;\u003c/code\u003e becomes \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003ccode\u003e\u0026lt;%= turbo_stream.animated_append \"todos\" do %\u0026gt;\u003c/code\u003e in \u003ccode\u003eapp/views/todos/new.html.erb\u003c/code\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd in \u003ccode\u003etodos_controller.rb\u003c/code\u003e, the destroy action changes from \u003ccode\u003eformat.turbo_stream { render turbo_stream: turbo_stream.remove(@todo) }\u003c/code\u003e to \u003ccode\u003eformat.turbo_stream { render turbo_stream: turbo_stream.animated_remove(@todo) }\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eProgressively Enhancing \u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe neat bit about this change is that we don’t need to completely rewrite the partials to get animations as the page changes. We go from portions of the page the appear suddenly or leave suddenly to a pleasant coming and going look to help the app feel more alive, and more similar to what you’d expect in a mobile application.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/03/26/hotwire-where-do-i-store-my-html-state","url":"https://onrails.blog/2024/03/26/hotwire-where-do-i-store-my-html-state","title":"HOTWire: Where do I store my HTML state?","date_published":"2024-03-26T13:58:48Z","date_modified":"2026-06-02T00:41:19Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe’re used to storing all of our data in the database, and letting Active Record pull it out, Action View to format it, and Action Controller to manage the request and response. But when we want quick client side interactivity, sometimes we need some extra data annotations on the HTML side that we can use without needing to communicate with the server. Most of this data is used by Stimulus, but Turbo has a few tags that can be useful.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/9wf9yXVJ2d8\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-4-3 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/9wf9yXVJ2d8\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003e\u003ccode\u003e\u0026lt;head\u0026gt;\u003c/code\u003e State\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOther Javascript frameworks, like Vue.js, React, and Angular, typically generate HTML on the client side, in Javascript. We can use the fact we’re sending server generated HTML Over The Wire to store information we might need in the \u003ccode\u003e'\u0026lt;head\u0026gt;\u003c/code\u003e tag of the page. Turbo will keep these values in sync as each page loads. Rails also has a \u003ccode\u003eprovides\u003c/code\u003e helper that can be used insert a tag into the head of the page. For example, if an \u003ccode\u003eaccount-id\u003c/code\u003e should be in the head, you can use this anywhere in a view:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;% provide :head, tag.meta(name: \"account-id\", content: \"1\") %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf your layout, like \u003ccode\u003eapplication.html.erb\u003c/code\u003e has this line:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= yield :head %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe account id meta tag will look like:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;meta name=\"account-id\" content=\"1\"\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, in a Stimulus controller, you could add a function like:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003efunction getMetaValue(name) {\n  const element = document.head.querySelector(`meta[name=\"${name}\"]`);\n  return element.getAttribute(\"content\");\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis will help read that value from the head, and you can include it wherever you want: \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003elet accountId =  getMetaValue(\"account-id\")\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eController State\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe Stimulus controller is a Javascript object, so properties can be set on the editor. You may have be using a third party library to create an object and keep it around during the controller’s life. For example, if you’re using \u003ca href=\"https://zurb.github.io/tribute/example/\"\u003eTribute\u003c/a\u003eto add mentions in a textfield, you can use \u003ccode\u003ethis\u003c/code\u003e and store it to a variable:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003econnect() {\n  this.tribute = new Tribute({\n    collection: []\n  });\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd in a different method, you can access the \u003ccode\u003etribute\u003c/code\u003e variable:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003eaddNames() {\n  this.tribute.appendCurrent([\n    { name: \"Howard Johnson\", occupation: \"Panda Wrangler\", age: 27 },\n    { name: \"Fluffy Croutons\", occupation: \"Crouton Fluffer\", age: 32 }\n  ]);\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eData Attributes\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eStimulus provides two mechanisms to make data available for your controller. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading {\"level\":3} --\u003e\n\u003ch3 class=\"wp-block-heading\"\u003eValues properties\u003c/h3\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe first is the \u003ca title=\"Values in the Stimulus Handbook\" href=\"https://stimulus.hotwired.dev/reference/values\"\u003e\u003ccode\u003evalues\u003c/code\u003e\u003c/a\u003e properties. These allow you to set parameters for the controller. For example, if you had a toggle like you would find in iOS, you may have an \u003ccode\u003eenabled\u003c/code\u003e attribute on it. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:columns --\u003e\n\u003cdiv class=\"wp-block-columns\"\u003e\u003c!-- wp:column --\u003e\n\u003cdiv class=\"wp-block-column\"\u003e\u003c!-- wp:image {\"id\":1091,\"linkDestination\":\"none\",\"align\":\"center\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA0OCwicHVyIjoiYmxvYl9pZCJ9fQ==--d50839325b15463307bad434765782dbd3f2513c/DraggedImage-6.png\" alt=\"Button Toggled Off\" class=\"wp-image-1091\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\u003c/div\u003e\n\u003c!-- /wp:column --\u003e\n\n\u003c!-- wp:column --\u003e\n\u003cdiv class=\"wp-block-column\"\u003e\u003c!-- wp:image {\"id\":1090,\"linkDestination\":\"none\",\"align\":\"center\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA0MiwicHVyIjoiYmxvYl9pZCJ9fQ==--22c0fe8ebd3eabbfd65a051d6c14b0909143aa89/DraggedImage-1-1.png\" alt=\"Button Toggled On\" class=\"wp-image-1090\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\u003c/div\u003e\n\u003c!-- /wp:column --\u003e\u003c/div\u003e\n\u003c!-- /wp:columns --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf the controller is named \u003ccode\u003etoggle\u003c/code\u003e, then adding a data attribute \u003ccode\u003eenabled\u003c/code\u003e looks like this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e \u0026lt;button type=\"button\" \n    data-controller=\"toggle\" \n    data-toggle-enabled-value=\"true\" \n...\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the controller, you add the \u003ccode\u003evalues\u003c/code\u003e target, and can access it anywhere in the controller:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e// Connects to data-controller=\"toggle\"\nexport default class extends Controller {\n  static values = { enabled: Boolean };\n\n  connect() {\n    if (this.enabledValue) {\n      console.log(\"This is enabled\");\n    }\n  }\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading {\"level\":3} --\u003e\n\u003ch3 class=\"wp-block-heading\"\u003eClasses properties\u003c/h3\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eSince changing classes is a common interactive enhancement, controllers often need to toggle classes. The \u003ca href=\"https://stimulus.hotwired.dev/reference/css-classes\"\u003e\u003ccode\u003eclasses\u003c/code\u003e\u003c/a\u003e properties assumes the values are strings, and will split apart multiple classes if they’re separated by a space. The button toggle example above needs four different classes to change, for the background color and the movement of the circle. Add these classes in the HTML:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e    data-toggle-enabled-class=\"bg-blue-600\"\n    data-toggle-disabled-class=\"bg-gray-200\"\n    data-toggle-enabled-translate-class=\"translate-x-5\"\n    data-toggle-disabled-translate-class=\"translate-x-0\"\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd then they’re accessible in the controller:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e  static classes = [\n    \"enabled\",\n    \"enabledTranslate\",\n    \"disabled\",\n    \"disabledTranslate\",\n  ];\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThey can be used in a method when you want to swap out classes:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e  setEnabled() {\n    this.frameTarget.classList.add(this.enabledClass);\n    this.frameTarget.classList.remove(this.disabledClass);\n    this.circleTarget.classList.add(this.enabledTranslateClass);\n    this.circleTarget.classList.remove(this.disabledTranslateClass);\n  }\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading {\"level\":3} --\u003e\n\u003ch3 class=\"wp-block-heading\"\u003eAction Params\u003c/h3\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eActions can also \u003ca href=\"https://stimulus.hotwired.dev/reference/actions#action-parameters\"\u003epass along values as parameters\u003c/a\u003e to the method that’s called. The toggle button has a \u003ccode\u003eswitch\u003c/code\u003e method that’s called when button is clicked. Adding an attribute, such as an id, could become available to the switch method:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003edata-action=\"toggle#switch\"\ndata-toggle-id-param=\"123\"\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhen the action fires, the params are added to the event:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e switch(event) {\n    console.log(event.params.id);\n  }\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eData everywhere\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThere are lots of options for passing data along from the server to the front end, and making it available for our Stimulus controllers. Our website can be interactive without the need for dipping into a heavier Javascript framework.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":3} /--\u003e\n\n\u003c!-- wp:woocommerce/handpicked-products {\"products\":[1016,876,1041]} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/03/26/hotwire-considering-morphing-or-turbo-frames","url":"https://onrails.blog/2024/03/26/hotwire-considering-morphing-or-turbo-frames","title":"HOTWire: Considering Morphing or Turbo Frames","date_published":"2024-03-26T09:00:00Z","date_modified":"2026-06-02T00:41:18Z","content_html":"\u003c!-- wp:image {\"id\":1007,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image size-large\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA1NiwicHVyIjoiYmxvYl9pZCJ9fQ==--6fa89d39b2fffff319a07e2473ef631d169814b8/HOTWire-Frame-New-Edit-Delete-1024x564.png\" alt=\"\" class=\"wp-image-1007\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWith the new morphing features in Turbo 8, you now need to decide on when to use Turbo streams or Turbo frames instead of full page refreshing. Thankfully, all three techniques work together. Let’s take the Todo app that we’ve been working on, and see where using Streams or Frames makes sense.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/y08mnpYrDmA\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-16-9 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/y08mnpYrDmA\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eTurbo Streams for a new Todo form\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe current new Todo interaction is a new page when clicking the \u003ccode\u003eNew Todo\u003c/code\u003e button on the top of the page. If we add the \u003ccode\u003edata-turbo-stream\u003c/code\u003e attribute, \u003ca href=\"https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses\"\u003ethe \u003ccode\u003eGET\u003c/code\u003e request now appears to the server as a \u003ccode\u003eTURBO_STREAM\u003c/code\u003e request\u003c/a\u003e. The response, \u003ccode\u003enew.turbo_stream.erb\u003c/code\u003e can append the form to the bottom of the list of Todos, and then when the form is submitted, the full page refresh will display the new Todo in the same place. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, add the \u003ccode\u003edata-turbo-stream\u003c/code\u003e to the \u003ccode\u003eNew Todo\u003c/code\u003e link on \u003ccode\u003etodos\\index.html.erb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= link_to \"New todo\", new_todo_path, class: \"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium\", data: {turbo_stream: true} %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen add the file \u003ccode\u003etodos\\new.turbo_stream.erb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= turbo_stream.append \"todos\" do %\u0026gt;\n  \u0026lt;%= turbo_frame_tag 'new_todo', target: \"_top\", class: \"flex items-center py-3 pl-3 bg-white border-b pr-11 gap-x-4 todo\" do %\u0026gt;\n    \u0026lt;%= render partial: 'form', locals: { todo: @todo } %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis will append the new form to the bottom of the Todos list. The \u003ccode\u003etarget\u003c/code\u003e is set to \u003ccode\u003e_top\u003c/code\u003e so that the successful creation and redirection bring the page back to the index action. Otherwise, since there wouldn’t be a Turbo Frame named \u003ccode\u003enew_todo\u003c/code\u003e, Turbo wouldn’t have anything to replace the interior content.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"align\":\"center\",\"id\":1000,\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA0OSwicHVyIjoiYmxvYl9pZCJ9fQ==--b572c93f6bd16e2cf870bafd4953e1431e7361b2/DraggedImage.png\" alt=\"\" class=\"wp-image-1000\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow we are using Turbo Streams to load a new form. The form, \u003ccode\u003etodos\\_form.html.erb\u003c/code\u003e, should be changed to fit inline:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= form_with(model: todo, class: \"flex-1 flex flex-row gap-x-4 items-center\") do |form| %\u0026gt;\n  \u0026lt;%= form.check_box :completed, class: \"ml-8\" %\u0026gt;\n  \u0026lt;div class=\"flex-1\"\u0026gt;\n    \u0026lt;%= form.text_field :name, class: \"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 w-full\" %\u0026gt;\n  \u0026lt;/div\u0026gt;\n  \u0026lt;div class=\"inline\"\u0026gt;\n    \u0026lt;%= form.submit class: \"rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer\" %\u0026gt;\n  \u0026lt;/div\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd you get a nice new Todo form. Clicking the Create Todo button saves the Todo, performs a redirect to the \u003ccode\u003eTodos#index\u003c/code\u003e, which Turbo morphs and shows the new Todo at the bottom of the list.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"align\":\"center\",\"id\":1003,\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA0MywicHVyIjoiYmxvYl9pZCJ9fQ==--72b124d3cb3ec4499b60198cbf66825c41607662/DraggedImage-1.png\" alt=\"\" class=\"wp-image-1003\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eEditing in place\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eUsing Turbo Frames, we don’t have to make many other changes to get in place editing of each Todo. First, change the \u003ccode\u003ediv\u003c/code\u003e of each Todo in \u003ccode\u003etodos\\_todo.html.erb\u003c/code\u003e to a \u003ccode\u003eturbo_frame_tag\u003c/code\u003e. The original:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;div id=\"\u0026lt;%= dom_id todo %\u0026gt;\" \n  class=\"flex items-center py-3 bg-white border-b gap-x-4 todo\" \n  data-reorderable-id=\"\u0026lt;%= todo.id %\u0026gt;\" \n  data-reorderable-path=\"\u0026lt;%= todo_priority_path(todo) %\u0026gt;\"\n  draggable=\"true\"\u0026gt;\n  \t\u0026lt;!-- rest of partial --\u0026gt;\n\u0026lt;/div\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003ebecomes:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= turbo_frame_tag todo, class: \"flex items-center py-3 bg-white border-b gap-x-4 todo\", data: { reorderable_path: todo_priority_path(todo), reorderable_id: todo.id }, draggable: true do %\u0026gt;\n\t\u0026lt;!-- rest of partial --\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe next change is to update the \u003ccode\u003etodos\\edit.html.erb\u003c/code\u003e partial to wrap the form in a \u003ccode\u003eturbo_frame_tag\u003c/code\u003e. Change the following lines:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e    \u0026lt;%= render \"form\", todo: @todo %\u0026gt;\n    \u0026lt;%= link_to \"Show this todo\", @todo, class: \"ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium\" %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd wrap the form like this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e    \u0026lt;%= turbo_frame_tag @todo, class: \"flex items-center py-3 bg-white border-b gap-x-4 todo\"  do %\u0026gt;\n      \u0026lt;%= turbo_stream_from @todo %\u0026gt;\n      \u0026lt;%= render \"form\", todo: @todo %\u0026gt;\n      \u0026lt;%= link_to \"Back\", @todo, class: \"mr-10 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium\" %\u0026gt;\n    \u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eClicking the “Edit” button will send a request for the \u003ccode\u003eedit.html.erb\u003c/code\u003e. Turbo will just get the contents of the frame, and replace the inside with the form. Clicking “Back” will make a request to the \u003ccode\u003eshow.html.erb\u003c/code\u003e, which has the same frame id since it renders the \u003ccode\u003e_todo.html.erb\u003c/code\u003e partial. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"align\":\"center\",\"id\":1002,\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA0NCwicHVyIjoiYmxvYl9pZCJ9fQ==--42f5dd41b40a1b833d403fb51a9516973f20d193/DraggedImage-2.png\" alt=\"\" class=\"wp-image-1002\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eDeleting Todos\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLastly, we can use Turbo actions to remove a Todo from the list. Add a delete button to the \u003ccode\u003e_todo.html.erb\u003c/code\u003e partial:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= link_to todo_path(todo), data: {turbo_method: \"delete\", turbo_confirm: \"Are you sure?\" }, class: \"rounded-full p-2 ml-2 bg-red-500 inline-block font-medium mr-1 text-red-50\" do %\u0026gt;\n    \u0026lt;svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" class=\"w-6 h-6\"\u0026gt;\n      \u0026lt;path fill-rule=\"evenodd\" d=\"M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z\" clip-rule=\"evenodd\" /\u0026gt;\n    \u0026lt;/svg\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis uses Heroicons trash icon and you get a nice delete button.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"align\":\"center\",\"id\":1001,\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA0NSwicHVyIjoiYmxvYl9pZCJ9fQ==--95a483d4f4859e0ee69d770581e91c239d02cb78/DraggedImage-3.png\" alt=\"\" class=\"wp-image-1001\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eClicking the trashcan will send the request to the \u003ccode\u003edestroy\u003c/code\u003e method on the \u003ccode\u003eTodosController\u003c/code\u003e, so add a new response in the \u003ccode\u003erespond_to\u003c/code\u003e block:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003eformat.turbo_stream { render turbo_stream: turbo_stream.remove(@todo) }\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis will send a response back that removes the Todo from the list.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003ePutting it all together\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTurbo Morphs fits right on top of how an existing app works. It layers on a new performance feel without needing to rewrite the who app to work with a new feature. That’s the best kind of upgrade.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/OnRailsBlog/todo_app/tree/8cea7e4d39a23d93ec50391f922baff0b7cf2fea\" target=\"_blank\" rel=\"noreferrer noopener\"\u003eYou can find the source code on Github here\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAnimating the Insertions and Deletions\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eYou can animate the additions and removals from the page easily. \u003ca href=\"https://onrails.blog/2024/04/01/hotwire-turbo-tutorial-animated-deletions-and-insertions/\"\u003eCheck out the tutorial here.\u003c/a\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/03/18/stimulus-moving-and-animating-todos","url":"https://onrails.blog/2024/03/18/stimulus-moving-and-animating-todos","title":"Stimulus Tutorial: Moving \u0026 Animating Todos","date_published":"2024-03-18T15:56:29Z","date_modified":"2026-06-02T00:41:16Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eDrag and drop functions are a fun interaction, but they may not be the best interface in every situation. Buttons are a great affordance, and we can hook them up into our existing drag and drop code without any issue. Then we’ll look into animating the movement on the page so that it still feels interactive. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/RGsvL3z_Lz8\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-16-9 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/RGsvL3z_Lz8\n\u003c/div\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eDemo of the movement when clicking the up and down buttons\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAdding Buttons\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOn the right side of each Todo row, let’s add an up and down arrow button. These will get their own actions, \u003ccode\u003emoveUp\u003c/code\u003e and \u003ccode\u003emoveDown\u003c/code\u003e on the Stimulus controller that will be refactored into moving the Todo up and down, and sending the change to the server. The icons come from \u003ca href=\"https://heroicons.com/\"\u003eHeroicons\u003c/a\u003e. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e  \u0026lt;div class=\"flex flex-col h-full mr-2 divide-y w-fit\"\u0026gt;\n    \u0026lt;button class=\"w-6 h-6 border rounded-t-full bg-gray-50 hover:bg-gray-300 up\"\n      data-action=\"reorder#moveUp\"\u0026gt;\n      \u0026lt;svg xmlns=\"http://www.w3.org/2000/svg\" \n      fill=\"none\" \n      viewBox=\"0 0 24 24\" \n      stroke-width=\"1.5\" \n      stroke=\"currentColor\" \u0026gt;\n        \u0026lt;path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4.5 15.75 7.5-7.5 7.5 7.5\" /\u0026gt;\n      \u0026lt;/svg\u0026gt;\n    \u0026lt;/button\u0026gt;\n    \u0026lt;button \n      class=\"w-6 h-6 border rounded-b-full bg-gray-50 hover:bg-gray-300 down\"\n      data-action=\"reorder#moveDown\"\u0026gt;\n      \u0026lt;svg xmlns=\"http://www.w3.org/2000/svg\" \n      fill=\"none\" \n      viewBox=\"0 0 24 24\" \n      stroke-width=\"1.5\" \n      stroke=\"currentColor\"\u0026gt;\n        \u0026lt;path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m19.5 8.25-7.5 7.5-7.5-7.5\" /\u0026gt;\n      \u0026lt;/svg\u0026gt;\n    \u0026lt;/button\u0026gt;\n  \u0026lt;/div\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eHiding Unnecessary Buttons\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf you just refresh the HTML, you’ll see the top row has a button to move up, and the bottom row has a button to move down. This doesn’t make sense, and looks sloppy. We can use the power of CSS to hide those buttons automatically, and save us some work. Add these lines to your CSS file:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"css\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003ediv.todo:first-of-type \u0026gt; div \u0026gt; button.up {\n  visibility: hidden;\n}\ndiv.todo:last-of-type \u0026gt; div \u0026gt; button.down {\n  visibility: hidden;\n}\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe outermost \u003ccode\u003ediv\u003c/code\u003e in \u003ccode\u003etodos\\_todo.html.erb\u003c/code\u003e gets a new class \u003ccode\u003etodo\u003c/code\u003e and each button has either the \u003ccode\u003eup\u003c/code\u003e or \u003ccode\u003edown\u003c/code\u003e class classes. We set up CSS selectors to hide the up button in the first Todo row, and the dow button on the last Todo row.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eUpdating the Stimulus controller\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe Stimulus controller gets refactored into a few new methods. The first is \u003ccode\u003emoveItems\u003c/code\u003e which moves the items on the page, and then performs the network update call.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003emoveItems(item, target) {\n    if (\n      target.compareDocumentPosition(item) \u0026amp; Node.DOCUMENT_POSITION_FOLLOWING\n    ) {\n      let result = target.insertAdjacentElement(\"beforebegin\", item);\n    } else if (\n      target.compareDocumentPosition(item) \u0026amp; Node.DOCUMENT_POSITION_PRECEDING\n    ) {\n      let result = target.insertAdjacentElement(\"afterend\", item);\n    }\n\n    let formData = new FormData();\n    formData.append(\"reorderable_target_id\", target.dataset.reorderableId);\n\n    fetch(item.dataset.reorderablePath, {\n      body: formData,\n      method: \"PATCH\",\n      credentials: \"include\",\n      dataType: \"script\",\n      headers: {\n        \"X-CSRF-Token\": getMetaValue(\"csrf-token\"),\n      },\n      redirect: \"manual\",\n    });\n  }\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003emoveDown\u003c/code\u003e and \u003ccode\u003emoveUp\u003c/code\u003e functions now use this \u003ccode\u003emoveItems\u003c/code\u003e function. First the item checks to see if there is a sibling node in the requested direction, and if that sibling has a reorderable id.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003easync moveUp(event) {\n    let parent = getDataNode(event.target);\n    if (\n      parent.previousSibling.dataset != null \u0026amp;\u0026amp;\n      parent.previousSibling.dataset.reorderableId != null\n    ) {\n      this.animateSwitch(parent.nextSibling, parent);\n      await Promise.all(\n        parent.getAnimations().map((animation) =\u0026gt; animation.finished)\n      );\n      this.moveItems(parent, parent.previousSibling);\n    }\n    event.preventDefault();\n  }\n\n  async moveDown(event) {\n    let parent = getDataNode(event.target);\n    if (\n      parent.nextSibling.dataset != null \u0026amp;\u0026amp;\n      parent.nextSibling.dataset.reorderableId != null\n    ) {\n       this.animateSwitch(parent.nextSibling, parent);\n      await Promise.all(\n        parent.getAnimations().map((animation) =\u0026gt; animation.finished)\n      );\n       this.moveItems(parent, parent.nextSibling);\n    }\n\n    event.preventDefault();\n  }\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe best part is the \u003ccode\u003eanimateSwitch\u003c/code\u003e method. It takes two rows, which it’s assumed are next to each other, and uses the \u003ccode\u003etransform\u003c/code\u003e property of an item to move the row either up or down in the vertical/Y direction. Once the animations are done, the \u003ccode\u003emoveItems\u003c/code\u003e will actually change the location of the rows in the DOM.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003eanimateSwitch(from, to) {\n    from.animate([{ transform: `translateY(-${from.clientHeight}px)` }], {\n      duration: 300,\n      easing: \"ease-in-out\",\n    });\n\n    to.animate([{ transform: `translateY(${to.clientHeight}px)` }], {\n      duration: 300,\n      easing: \"ease-in-out\",\n    });\n  } \n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAccessible and Interactive\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, we have buttons that someone can use to move the Todos up and down, and we can use animation as an affordance that the change happened. And we use basic animations, rather than needing to import a whole UI framework. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eYou can see all the changes on \u003ca href=\"https://github.com/OnRailsBlog/todo_app/tree/04ad04b0a24d07f0a246744d2034ade3cf0f8b26\"\u003eGithub\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":3} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/03/14/62-hotwire-tutorial-listening-for-changes-over-actioncable","url":"https://onrails.blog/2024/03/14/62-hotwire-tutorial-listening-for-changes-over-actioncable","title":"HOTWire Tutorial: Listening for changes over ActionCable","date_published":"2024-03-14T15:49:59Z","date_modified":"2026-06-02T00:41:15Z","content_html":"\u003c!-- wp:image {\"id\":976,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image size-large\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA1MywicHVyIjoiYmxvYl9pZCJ9fQ==--190b2a42ba70811717b56bb2e5296326f6f61cc2/HOTWire-Broadcasts-Sample-1024x569.png\" alt=\"\" class=\"wp-image-976\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eMany of \u003ca href=\"https://github.com/hotwired/turbo/releases/tag/v8.0.0\"\u003ethe changes in Turbo 8\u003c/a\u003e are incredibly promising for improving the perception of speed and interactivity on our web apps. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eA lot of my Stimulus Tutorials need an update since they were first written, so follow along to update existing tutorials and rethink them with the newest tools available. Today we obsolete the tutorials \u003ca href=\"https://onrails.blog/2018/12/14/grabbing-actioncable-with-stimulus-js/\"\u003eGrabbing ActionCable with Stimulus.js\u003c/a\u003e and \u003ca href=\"https://onrails.blog/2019/01/23/subscribing-to-many-channels-in-actioncable/\"\u003eSubscribing to many channels in ActionCable\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/HnJef2x4HVA\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-16-9 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/HnJef2x4HVA\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eMoving on from the Stimulus controllers \u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe initial tutorials required creating a Stimulus controller and calling the ActionCable code to setup the web socket connection. Now, Turbo automatically sets up those connections if the HTML includes the \u003ccode\u003eturbo_stream_from\u003c/code\u003e directive. In the Todo example, this means we could add the following to \u003ccode\u003etodos\\_todo.html\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= turbo_stream_from todo %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe generated HTML will look something like:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;turbo-cable-stream-source channel=\"Turbo::StreamsChannel\" \nsigned-stream-name=\"IloybGtPaTh2ZEc5a2J5MWhjSEF2Vkc5a2J5OHhNQSI=--d126352451e175c364445a3c1f22a1529c3243e4b8a686890f6783814af09e37\" \nconnected=\"\"\u0026gt;\n\u0026lt;/turbo-cable-stream-source\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTurbo has its own channel that will subscribe to all the names, and the channel name gets encrypted to protect from someone trying to guess the value.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eNo extra Channels\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe previous examples also required setting up a ActionCable channel to stream events. Since we use the Turbo channel, this is no longer required. Instead, the model that needs to stream updates adds a helper method, and on creation, updates, and deletion, it gets events broadcasted over the Turbo channel. The change in Turbo 8 is that instead of needing to send new HTML over the web socket, the page refreshes and morphs the new elements to make it appear like everything changed.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe Todo item needs \u003ccode\u003ebroadcasts_refreshes\u003c/code\u003e and all these updates are sent automatically to the front end for those Todos that are listened to.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eListening for new Todos\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhen creating a new Todo, there isn’t an existing channel listening for that Todo. Turbo handles this by broadcasting creations to a model specific channel, \u003ccode\u003e\"todos\"\u003c/code\u003e in this case. In \u003ccode\u003etodos\\index.html.erb\u003c/code\u003e: adding the stream from directive will load in the Todos to the list:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e\u0026lt;%= turbo_stream_from Todo.model_name.plural %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eTouch to broadcast changes\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhen dragging and dropping the Todos, the \u003ccode\u003eupsert\u003c/code\u003e operation doesn’t automatically toggle a broadcast to make the front end refresh. By calling the touch method on the todo that was dragged, the front end is notified of the change, and the page refreshes with the updates.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code\"\u003e@todo.touch\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eSimplifying \u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe changes to Turbo over the past few years have allowed for better interactivity and better developer speed. These improvements are exceptional.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/OnRailsBlog/todo_app/tree/d344ffbd8420016b722508175317949baa27a663\" target=\"_blank\" rel=\"noreferrer noopener\"\u003eYou can see the code on Github.\u003c/a\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/03/08/hotwire-tutorial-how-do-i-drag-and-drop-items-in-a-list","url":"https://onrails.blog/2024/03/08/hotwire-tutorial-how-do-i-drag-and-drop-items-in-a-list","title":"Hotwire Tutorial: How Do I Drag and Drop Items in a List?","date_published":"2024-03-08T22:38:02Z","date_modified":"2026-06-02T00:41:15Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf you’ve been following \u003ca href=\"https://github.com/hotwired/turbo/releases/tag/v8.0.0\"\u003ethe changes in Turbo 8\u003c/a\u003e, it looks incredibly promising for improving the perception of speed and interactivity on our web apps. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eA lot of the Stimulus Tutorials could use an update since they were first written, so I thought it would be good to over existing tutorials and rethink them with the newest tools available. Join me as we rebuild the Stimulus Tutorial “\u003ca title=\"How do I Drag and Drop Items in a list\" href=\"https://onrails.blog/2018/03/09/stimulus-js-tutorial-how-do-i-drag-and-drop-items-in-a-list/\"\u003eHow do I Drag and Drop Items in a list\u003c/a\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/riBrFASNaHs\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-4-3 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/riBrFASNaHs\n\u003c/div\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eClient side tutorial\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/NuWWk6iWgwg\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-4-3 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/NuWWk6iWgwg\n\u003c/div\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eServer side Tutorial\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eSetting Priority\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe’ll build off the previous example and add a priority order. This will be used sort the Todos.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e$ rails g migration AddPriorityToTodo priority:integer\n$ rails db:migrate\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf there are existing Todos in the database, go ahead and set the priority to the id of the existing Todo:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e$ rails c\n* Todo.all.each do |todo|\n*   todo.priority = todo.id\n*   todo.save\n\u0026gt; end\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the \u003ccode\u003etodos_controller.rb\u003c/code\u003e, make the \u003ccode\u003eindex\u003c/code\u003e method sort the Todos by this new priority field:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003edef index\n  @todos = Todo.all.order(\"priority\")\nend\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd when creating a new Todo, we should set the priority to its ID after creating it by using a callback. This is a simplification since we only have a single Todo model in our application, and you may want to set priority based on other models that you design, like a Project or a List.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003eclass Todo \u0026lt; ApplicationRecord\n  after_create :set_priority\n\n  private\n\n  def set_priority\n    self.priority = id\n    save\n  end\nend\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThere are two parts we need to build next. A stimulus controller for the front end, and new rails controller to shuffle around the Todos and put them in the correct order by priority. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eReorder Stimulus Controller\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLike in the previous drag and drop tutorial, the controller needs to listen to a number of events to handle the dragging of an item. You can generate it with this command:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e$ rails g stimulus reorder\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe controller will use the \u003ca href=\"https://stimulus.hotwired.dev/reference/css-classes\"\u003ecss classes\u003c/a\u003e feature to apply styling as the list of Todos becomes active, as the dragged item moves around, and as a potential drop target is hovered over. This creates a very interactive effect, and helps show what the drag is doing. These go at the top of the controller:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  static classes = [\"activeDropzone\", \"activeItem\", \"dropTarget\"];\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe first event the controller listens for is \u003ccode\u003edragstart\u003c/code\u003e. The \u003ccode\u003edragstart()\u003c/code\u003e method will add the CSS classes to the element and the dragged item to provide more visual information as the drag happens. The drag event can transfer information, and the controller will set the id of the item that’s being dragged so when it can be referred to when it’s dropped.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  dragstart(event) {\n    this.element.classList.add(...this.activeDropzoneClasses);\n    const draggableItem = getDataNode(event.target);\n    draggableItem.classList.add(...this.activeItemClasses);\n    event.dataTransfer.setData(\n      \"application/drag-key\",\n      draggableItem.dataset.reorderableId\n    );\n    event.dataTransfer.effectAllowed = \"move\";\n  } \n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe next event is \u003ccode\u003edragover\u003c/code\u003e. It’s important to listen and respond to \u003ccode\u003etrue\u003c/code\u003e so that we can eventually drop the Todo in the list.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  dragover(event) {\n    event.preventDefault();\n    return true;\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAt the bottom of the controller, there is a helper helper to get the parent div of the Todo that holds the reorderable id:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003efunction getDataNode(node) {\n  return node.closest(\"[data-reorderable-id]\");\n} \n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis is used in the next few methods. \u003ccode\u003edragenter\u003c/code\u003e and \u003ccode\u003edragleave\u003c/code\u003e work together to change the appearance of the potential drop target. Both find the Todo node in the DOM. \u003ccode\u003edragenter()\u003c/code\u003e adds the drop target styling with CSS classes, and then sets all the children pointer events to none so that the inner items, like the checkbox, the text, and the buttons don’t fire their own \u003ccode\u003edragenter\u003c/code\u003e events. \u003ccode\u003edragleave()\u003c/code\u003e removes the CSS classes, and unset’s the pointer events on the children elements.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  dragenter(event) {\n    let parent = getDataNode(event.target);\n    if (parent != null \u0026amp;\u0026amp; parent.dataset.reorderableId != null) {\n      parent.classList.add(...this.dropTargetClasses);\n      for (const child of parent.children) {\n        child.classList.add(\"pointer-events-none\");\n      }\n      event.preventDefault();\n    }\n  }\n\n  dragleave(event) {\n    let parent = getDataNode(event.target);\n    if (parent != null \u0026amp;\u0026amp; parent.dataset.reorderableId != null) {\n      parent.classList.remove(...this.dropTargetClasses);\n      for (const child of parent.children) {\n        child.classList.remove(\"pointer-events-none\");\n      }\n\n      event.preventDefault();\n    }\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003edrop()\u003c/code\u003e method uses a helper method to get the \u003ca href=\"https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf\"\u003eCSRF\u003c/a\u003e token:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003efunction getMetaValue(name) {\n  const element = document.head.querySelector(`meta[name=\"${name}\"]`);\n  return element.getAttribute(\"content\");\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003edrop\u003c/code\u003e method gets the id the Todo that was dragged and the Todo that was dropped down. It doesn’t do anything special to validate the ordering, but it uses the fetch API to send an update to the rails controller we’ll add in the next section. It also repositions the dragged item in the DOM by comparing the document position and then inserting the item as an adjacent element. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  drop(event) {\n    this.element.classList.remove(...this.activeDropzoneClasses);\n\n    const dropTarget = getDataNode(event.target);\n    dropTarget.classList.remove(...this.dropTargetClasses);\n    for (const child of dropTarget.children) {\n      child.classList.remove(\"pointer-events-none\");\n    }\n\n    var data = event.dataTransfer.getData(\"application/drag-key\");\n    const draggedItem = this.element.querySelector(\n      `[data-reorderable-id='${data}']`\n    );\n\n    if (draggedItem) {\n      draggedItem.classList.remove(...this.activeItemClasses);\n\n      if (\n        dropTarget.compareDocumentPosition(draggedItem) \u0026amp;\n        Node.DOCUMENT_POSITION_FOLLOWING\n      ) {\n        let result = dropTarget.insertAdjacentElement(\n          \"beforebegin\",\n          draggedItem\n        );\n      } else if (\n        dropTarget.compareDocumentPosition(draggedItem) \u0026amp;\n        Node.DOCUMENT_POSITION_PRECEDING\n      ) {\n        let result = dropTarget.insertAdjacentElement(\"afterend\", draggedItem);\n      }\n\n      let formData = new FormData();\n      formData.append(\n        \"reorderable_target_id\",\n        dropTarget.dataset.reorderableId\n      );\n\n      fetch(draggedItem.dataset.reorderablePath, {\n        body: formData,\n        method: \"PATCH\",\n        credentials: \"include\",\n        dataType: \"script\",\n        headers: {\n          \"X-CSRF-Token\": getMetaValue(\"csrf-token\"),\n        },\n        redirect: \"manual\",\n      });\n    }\n    event.preventDefault();\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFinally, the \u003ccode\u003edragend\u003c/code\u003e event is used to remove CSS classes:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  dragend(event) {\n    this.element.classList.remove(...this.activeDropzoneClasses);\n  }\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003ePriority Controller\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, put in a new directive to the \u003ccode\u003eroutes.rb\u003c/code\u003e file to add a route to the \u003ccode\u003epriorities_controller.rb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e  resources :todos do\n    resource :priority, only: [:update]\n  end\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003epriorities_controller.rb\u003c/code\u003e will only have one method, \u003ccode\u003eupdate\u003c/code\u003e. It will find the Todo that was dragged, and the Todo that was the drop target. Then, depending on the ordering, the priority on the all the Todos between those two items are swapped. The Upset command is used to update all the Todos at once. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003eclass PrioritiesController \u0026lt; ApplicationController\n  def update\n    @todo = Todo.find_by_id params[:todo_id]\n    @new_priority_todo = Todo.find_by_id params[:reorderable_target_id]\n\n    if !@new_priority_todo.nil?\n      old_priority = @todo.priority\n      new_priority = @new_priority_todo.priority\n\n      if old_priority \u0026gt; new_priority\n        todos = todos(new_priority..old_priority)\n        (0..(todos.length - 2)).each do |i|\n          first_todo = todos[i]\n          second_todo = todos[i + 1]\n          temp_priority = first_todo[:priority]\n          first_todo[:priority] = second_todo[:priority]\n          second_todo[:priority] = temp_priority\n        end\n        todos[todos.length - 1][:priority] = new_priority\n        Todo.upsert_all(todos)\n      elsif old_priority \u0026lt; new_priority\n        todos = todos(old_priority..new_priority)\n        (todos.length - 1).downto(1).each do |i|\n          first_todo = todos[i - 1]\n          second_todo = todos[i]\n          temp_priority = first_todo[:priority]\n          second_todo[:priority] = first_todo[:priority]\n          second_todo[:priority] = temp_priority\n        end\n        todos[0][:priority] = new_priority\n        Todo.upsert_all(todos)\n      end\n    end\n  end\n\n  private\n\n  def todos(range)\n    Todo.where(priority: range).order(:priority)\n      .pluck(:id, :priority, :updated_at, :created_at)\n      .map { |id, priority, created_at, updated_at| {id: id, priority: priority, updated_at: updated_at, created_at: created_at} }\n  end\nend\n\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eWiring up the Front End\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFinally, let’s add the annotations Stimulus needs in order to work. First, add the controller directives in \u003ccode\u003etodos\\index.html.erb\u003c/code\u003e. Note the six drag actions that direct the drag and drop actions. The CSS classes are also setup with a number of Tailwind CSS classes. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\u0026lt;div id=\"todos\" \n    class=\"min-w-full mt-1 overflow-hidden border rounded-lg\" \n    data-controller=\"reorder\"\n    data-action=\"dragstart-\u0026gt;reorder#dragstart dragover-\u0026gt;reorder#dragover dragenter-\u0026gt;reorder#dragenter dragleave-\u0026gt;reorder#dragleave drop-\u0026gt;reorder#drop dragend-\u0026gt;reorder#dragend\"\n    data-reorder-active-dropzone-class=\"border-dashed bg-gray-50 border-slate-400\"\n    data-reorder-active-item-class=\"shadow\"\n    data-reorder-drop-target-class=\"shadow-inner shadow-gray-500\"\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003etodos/_todo.html.erb\u003c/code\u003e needs three values, and then a visual drag icon is added. The first one is setting \u003ccode\u003edraggable\u003c/code\u003e to \u003ccode\u003etrue\u003c/code\u003e, and set the \u003ccode\u003ereorderable-id\u003c/code\u003e and the \u003ccode\u003ereorderable-path\u003c/code\u003e that are used by Stimulus.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\u0026lt;div id=\"\u0026lt;%= dom_id todo %\u0026gt;\" \n  class=\"flex items-center py-3 bg-white border-b gap-x-4\" \n  data-reorderable-id=\"\u0026lt;%= todo.id %\u0026gt;\" \n  data-reorderable-path=\"\u0026lt;%= todo_priority_path(todo) %\u0026gt;\"\n  draggable=\"true\"\u0026gt;\n  \u0026lt;div class=\"w-4 h-full -my-3 text-gray-900 rounded-sm hover:bg-gray-300 active:bg-gray-400 hover:cursor-grab active:cursor-grabbing\" \u0026gt;\n    \u0026lt;svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"w-4 h-10\"\u0026gt;\n      \u0026lt;path d=\"M8 2a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM8 6.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM9.5 12.5a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0Z\" /\u0026gt;\n    \u0026lt;/svg\u0026gt;\n  \u0026lt;/div\u0026gt;\n\n\u0026lt;!--... remaining Todo HTML is untouched --\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow there is some very interactive Drag and Drop functionality on the Todo list.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/GEig8DlS5xs\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-16-9 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/GEig8DlS5xs\n\u003c/div\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eQuick demo of Drag and Drop functionality\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/OnRailsBlog/todo_app/tree/04ad04b0a24d07f0a246744d2034ade3cf0f8b26\" target=\"_blank\" rel=\"noreferrer noopener\"\u003eYou can find the code on Github here.\u003c/a\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2024/03/06/stimulusjs-tutorial-update-model-with-checkbox-using-turbo-morphing","url":"https://onrails.blog/2024/03/06/stimulusjs-tutorial-update-model-with-checkbox-using-turbo-morphing","title":"Stimulus.js and HotWired Tutorial: Update Model with Checkbox using Turbo Morphing","date_published":"2024-03-06T14:28:00Z","date_modified":"2026-06-02T00:41:10Z","content_html":"\u003c!-- wp:image {\"id\":840,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} --\u003e\n\u003cfigure class=\"wp-block-image size-large\"\u003e\u003ca href=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA1OSwicHVyIjoiYmxvYl9pZCJ9fQ==--c8326ebfd82238e92792a554ccd62f1f5ec6d2fd/Stimulus-JS-Turbo-Morph-Remote-Checkbox-Tutorial.png\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTA1OCwicHVyIjoiYmxvYl9pZCJ9fQ==--fec6d21de75f394482b3858cc07881f4ba0a25f2/Stimulus-JS-Turbo-Morph-Remote-Checkbox-Tutorial-1024x700.png\" alt=\"\" class=\"wp-image-840\"\u003e\u003c/a\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf you’ve been following \u003ca href=\"https://github.com/hotwired/turbo/releases/tag/v8.0.0\"\u003ethe changes in Turbo 8\u003c/a\u003e, it looks incredibly promising for improving the perception of speed and interactivity on our web apps. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eA lot of the Stimulus Tutorials could use an update since they were first written, so I thought it would be good to over existing tutorials and rethink them with the newest tools available. Join me as we rebuild the first Stimulus Tutorial, and \u003ca href=\"https://onrails.blog/2020/11/09/updated-tutorial-how-do-i-update-my-model-from-a-checkbox/\"\u003eit’s updated version\u003c/a\u003e, “\u003ca href=\"https://onrails.blog/2018/03/13/stimulus-js-tutorial-how-do-i-update-my-model-from-a-checkbox/\"\u003eHow do I Remotely Update My Model from a checkbox\u003c/a\u003e”.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/dB7vFHX7aqY\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-4-3 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/dB7vFHX7aqY\n\u003c/div\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, start by creating a fresh Rails app. I’m using Rails 7.1 and Ruby 3.3.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e$ rails new todo_app -c tailwind\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe need to update the \u003ccode\u003eapplication.html.erb\u003c/code\u003e file to use the Turbo 8 Morphing by adding these lines inside the \u003ccode\u003e\u0026lt;head\u0026gt;\u003c/code\u003e tag.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\u0026lt;% turbo_refreshes_with method: :morph, scroll: :preserve %\u0026gt;\n\u0026lt;%= yield :head %\u0026gt;\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLet’s scaffold out a Todo Item that has a name and its completed state.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e$ rails g scaffold Todo name:string completed:boolean\n$ rails db:migrate \u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe default views for the Todo could use some tweaking to make it look more like actionable item, so update the \u003ccode\u003etodos/_todo.html.erb\u003c/code\u003e view to this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\u0026lt;div id=\"\u0026lt;%= dom_id todo %\u0026gt;\" class=\"flex items-center py-3 border-b gap-x-4\"\u0026gt;\n  \u0026lt;input type=\"checkbox\" \u0026lt;%= \"checked\" if todo.completed %\u0026gt;\u0026gt;\n  \u0026lt;span class=\"flex-1 \"\u0026gt;\u0026lt;%= todo.name %\u0026gt;\u0026lt;/span\u0026gt;\n  \u0026lt;% if action_name != \"show\" %\u0026gt;\n    \u0026lt;%= link_to \"Show this todo\", todo, class: \"rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium\" %\u0026gt;\n    \u0026lt;%= link_to \"Edit this todo\", edit_todo_path(todo), class: \"rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium\" %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe need to tweak the checkbox so that changing the value will be updated on the server. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eChange the checkbox input to a small form that will trigger a form submission when it’s toggled.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e\u0026lt;div id=\"\u0026lt;%= dom_id todo %\u0026gt;\" class=\"flex items-center py-3 border-b gap-x-4\"\u0026gt;\n  \u0026lt;%= form_with model: todo, data: { controller: \"form\" } do |form| %\u0026gt;\n    \u0026lt;%= form.label :completed, for: dom_id(todo, :completed) do %\u0026gt;\n      \u0026lt;%= form.check_box :completed, id: dom_id(todo, :completed), data: { action: \"form#submit\" } %\u0026gt;\n    \u0026lt;% end %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n  \u0026lt;span class=\"flex-1 \"\u0026gt;\u0026lt;%= todo.name %\u0026gt;\u0026lt;/span\u0026gt;\n  \u0026lt;% if action_name != \"show\" %\u0026gt;\n    \u0026lt;%= link_to \"Show this todo\", todo, class: \"rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium\" %\u0026gt;\n    \u0026lt;%= link_to \"Edit this todo\", edit_todo_path(todo), class: \"rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium\" %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAdd a Stimulus controller, \u003ccode\u003eform_controller.js\u003c/code\u003e that will submit the form when the checkbox is changed:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003eimport { Controller } from \"@hotwired/stimulus\";\n\nexport default class extends Controller {\n  submit() {\n    this.element.requestSubmit();\n  }\n}\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eChange the \u003ccode\u003etodos_controller.rb\u003c/code\u003e update method to redirect back to the Todos list:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:code --\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e def update\n    respond_to do |format|\n      if @todo.update(todo_params)\n        format.html { redirect_to todos_url, notice: \"Todo was successfully updated.\" }\n        format.json { render :show, status: :ok, location: @todo }\n      else\n        format.html { render :edit, status: :unprocessable_entity }\n        format.json { render json: @todo.errors, status: :unprocessable_entity }\n      end\n    end\n  end\n\u003c/code\u003e\u003c/pre\u003e\n\u003c!-- /wp:code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, we get the same benefits of a remotely filled in form, while using leaning on Turbo’s morphing ability.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":3} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2022/11/07/displaying-progress-in-a-long-running-background-job-hotwire","url":"https://onrails.blog/2022/11/07/displaying-progress-in-a-long-running-background-job-hotwire","title":"Displaying Progress in a Long Running Background Job using HOTWire","date_published":"2022-11-07T14:31:42Z","date_modified":"2026-06-02T00:41:10Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFast UIs need to feel like a lot is happening, even if that’s in the background and out of site of your user. On a mobile app, usually that means moving network calls outside the “main thread” and into a background thread that calls back. On websites, it means making every request back to the server as short as possible, and then waiting for some update back from the server. This could be accomplished by polling the server, but with ActionCable and Turbo Streams, you can let the server do whatever background work is needed and have it push those updates to you. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOne example is processing CSV files from your users. Sometimes APIs are great for getting data into your app, and you can’t beat comma separated values for updating records. The general idea is updating several records based off rows in a spreadsheet. This can be broken down into two steps: upload the file, and process the file. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"id\":821,\"sizeSlug\":\"full\",\"linkDestination\":\"media\"} --\u003e\n\u003cfigure class=\"wp-block-image size-full\"\u003e\u003ca href=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAzMywicHVyIjoiYmxvYl9pZCJ9fQ==--02cd8f9104ade54c54bf9b2c1fc92ae7e678c210/04.2-HOTWire-Background-Job-Loading-Animation.gif\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAzMywicHVyIjoiYmxvYl9pZCJ9fQ==--02cd8f9104ade54c54bf9b2c1fc92ae7e678c210/04.2-HOTWire-Background-Job-Loading-Animation.gif\" alt=\"\" class=\"wp-image-821\"\u003e\u003c/a\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eUploading the file can be done with JavaScript and a loading bar, so we know when the file is done uploading, and how much more progress there is. Processing the file happens on the backend, and that’s where getting visibility into the process can be tough. Do we create a database record that we use to keep track of the progress? Does that record get purged after the upload and processing are complete? Those are business problems that can be different depending on the situation. But given that the server that renders a webpage might be different from the server that processes the file, putting the file into a cloud bucket makes the most sense, at least until we’re done with the processing. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOn the server side, using web sockets to communicate progress back to the front end can provide a sense of progress, and relief that something is actually happening, whether it’s 10 or 10,000 rows in the spreadsheet. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eYou can probably come up with other processes that need to display background progress, such sending out batches of notifications, but let’s work through processing books like \u003ca href=\"https://onrails.blog/2022/10/31/55-adding-loading-screen-with-turbo/\"\u003ein our previous Book example.\u003c/a\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eUploading a CSV file\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe can use ActiveStorage to help get the file into our system, and to help keep track of progress throughout the upload and processing. Each Blob in ActiveStorage has a database record, and can be purged once the whole process is complete. ActiveStorage also has a mechanism to put the file directly into the cloud or file storage we’re using, and it emits off progress events that we can use to show a loading indicator on the page. Once the file is uploaded, then we send the key back to the server and start processing the file. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eStart with a clean app:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ rails new BookImports -c tailwind\n$ cd BookImports \n$ rails g scaffold Book title:string author:string publisher:string category:string isbn:string dewey_decimal_number:string binding:integer\n$ bin/rails active_storage:install \n$ rails db:migrate\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe’re going to make binding an enum since there are only a few options, so change \u003ccode\u003emodels/book.rb\u003c/code\u003e to this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eclass Book \u0026lt; ApplicationRecord\n  enum binding: [:hardcover, :paperback]\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTo test our system with plenty of books, We will use the Faker gem to create about 100 fake books in a CSV file that we can upload.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn our Gemfile:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003egem 'faker'\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen run\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ bundle install\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen we’ll create the books in our \u003ccode\u003edb/seeds.rb\u003c/code\u003e file.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003erequire 'csv'\n\npath = 'public/books.csv'\n\nCSV.open(path, 'w', headers: ['title', 'author', 'publisher', 'category', 'isbn', 'dewey_decimal_number', 'binding'], write_headers: true) do |csv|\n  100.times do |i|\n    csv \u0026lt;\u0026lt; [Faker::Book.title, Faker::Book.author, Faker::Book.publisher, Faker::Book.genre, \"#{Faker::Number.number(digits: 3)}-#{Faker::Number.number(digits: 1)}-#{Faker::Number.number(digits: 2)}-#{Faker::Number.number(digits: 6)}-#{Faker::Number.number(digits: 1)}\", \"#{Faker::Number.number(digits: 3)}.#{Faker::Number.number(digits: 3)}\", Faker::Number.between(from: 0, to: 1)]\n  end\nend \u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWorking on the import pages, generate the controller:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ rails g controller Import show create\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eChange the route for \u003ccode\u003ecreate\u003c/code\u003e into a post in \u003ccode\u003eroutes.rb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003epost 'import/create'\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the form, we’re going to set it as multipart, so we can upload a file, but first, let’s create the Stimulus controller that will show the upload progress.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ rails g stimulus uploads\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAdd the ActiveStorage direct upload library with your current JavaScript management tool, for example with importmap:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ ./bin/importmap pin @rails/activestorage\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003ca href=\"https://edgeguides.rubyonrails.org/active_storage_overview.html#usage\"\u003eAdd ActiveStorage to your \u003ccode\u003ejavascript/application.js\u003c/code\u003e:\u003c/a\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eimport * as ActiveStorage from \"@rails/activestorage\";\nActiveStorage.start();\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003euploads_controller.js\u003c/code\u003e is going to listen for two events: \u003ccode\u003edirect-upload:initialize@window\u003c/code\u003e which will trigger showing the progress indicator, and \u003ccode\u003edirect-upload:progress@window\u003c/code\u003e, which will give us the current upload progress. The controller has a wrapper around the progress indicator and the progress indicator itself, which are updated by these events.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eimport { Controller } from \"@hotwired/stimulus\"\n\n// Connects to data-controller=\"uploads\"\nexport default class extends Controller {\n  static targets = [\"progress\", \"progressWrapper\"];\n\n  initializeDirectUpload() {\n    this.progressWrapperTarget.classList.remove(\"hidden\");\n  }\n\n  progressDirectUpload(event) {\n    const { id, progress } = event.detail;\n    this.progressTarget.style.width = `${progress}%`;\n  }\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe form, \u003ccode\u003eviews/import/show.html.erb\u003c/code\u003e, is as follows:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div class=\"max-w-3xl mx-auto\"\u0026gt;\n  \u0026lt;h1 class=\"font-bold text-4xl\"\u0026gt;Import Books\u0026lt;/h1\u0026gt;\n  \u0026lt;%= form_with url: import_create_path, multipart: true do %\u0026gt;\n    \u0026lt;div class=\"divide-y divide-gray-200 space-y-5\"\u0026gt;\n      \u0026lt;div class=\"mt-5 space-y-5\"\u0026gt;\n        \u0026lt;div class=\"grid grid-cols-3 gap-4 items-start border-t border-gray-200 pt-5\"\u0026gt;\n          \u0026lt;label  class=\"block text-sm font-medium text-gray-700 mt-px pt-2\"\u0026gt;Data File (CSV):\u0026lt;/label\u0026gt;\n          \u0026lt;div class=\"mt-0 col-span-2\" \n            data-controller='uploads' \n            data-action='direct-upload:initialize@window-\u0026gt;uploads#initializeDirectUpload  direct-upload:progress@window-\u0026gt;uploads#progressDirectUpload' \u0026gt;\n            \u0026lt;div class=\"max-w-lg flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md\"\u0026gt;\n              \u0026lt;div class=\"space-y-1 text-center\"\u0026gt;\n                \u0026lt;div class=\"w-full flex text-sm text-gray-600\"\u0026gt;\n                  \u0026lt;label for=\"file\" class=\"relative cursor-pointer bg-white rounded-md font-medium text-blue-700 hover:text-blue-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-blue-500\"\u0026gt;\n                    \u0026lt;%= file_field_tag :file, class: 'input', direct_upload: true %\u0026gt;\n                  \u0026lt;/label\u0026gt;\n                \u0026lt;/div\u0026gt;\n                \u0026lt;div class=\"w-full pt-1 hidden\" \n                     data-uploads-target=\"progressWrapper\"\u0026gt;\n                  \u0026lt;span\u0026gt;Uploading file\u0026lt;/span\u0026gt;\n                  \u0026lt;div class=\"overflow-hidden w-60 h-4 text-xs flex rounded bg-blue-200\"\u0026gt;\n                    \u0026lt;div data-uploads-target=\"progress\"\n                         style=\"width: 0%\"\n                         class=\" shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-blue-800\" \u0026gt;\u0026lt;/div\u0026gt;\n                  \u0026lt;/div\u0026gt;\n                \u0026lt;/div\u0026gt;\n              \u0026lt;/div\u0026gt;\n            \u0026lt;/div\u0026gt;\n          \u0026lt;/div\u0026gt;\n        \u0026lt;/div\u0026gt;\n        \u0026lt;div class=\"pt-5\"\u0026gt;\n          \u0026lt;div class=\"flex justify-end\"\u0026gt;\n            \u0026lt;%= submit_tag \"Upload File\", class: \"inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500\" %\u0026gt;\n          \u0026lt;/div\u0026gt;\n        \u0026lt;/div\u0026gt;\n      \u0026lt;/div\u0026gt;\n    \u0026lt;/div\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;/div\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAt this point, when you upload the file, you get a nice progress indicator, but the controller doesn’t respond appropriately, so nothing happens on the page. We need to handle the uploaded file. Let’s start by creating a background job that will process the CSV.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ bin/rails generate job ImportBooks\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003ecreate\u003c/code\u003e action on the import controller will queue the import job by passing in the blob, and then pass back the HTML that will display the progress indicator, and create the ActionCable channel that will listen for progress updates. Let’s wrap the form from the \u003ccode\u003eshow.html.erb\u003c/code\u003e page with a \u003ccode\u003eturbo_frame_tag\u003c/code\u003e so that the response can replace that part of the response.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div\u0026gt;\n  \u0026lt;h1 class=\"font-bold text-4xl\"\u0026gt;Import Books\u0026lt;/h1\u0026gt;\n  \u0026lt;%= turbo_frame_tag \"import\" do %\u0026gt;\n    \u0026lt;%= form_with url: import_create_path, multipart: true do %\u0026gt;\n      \u0026lt;!-- the rest of the form is ommitted for brevity --\u0026gt;\n    \u0026lt;% end %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;/div\u0026gt; \u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003ecreate\u003c/code\u003e template should be renamed to \u003ccode\u003ecreate.turbo_stream.erb\u003c/code\u003e and will have:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;%= turbo_stream.replace \"import\" do %\u0026gt;\n  \u0026lt;%= turbo_stream_from @blob.key %\u0026gt;\n  \u0026lt;div class=\"divide-y divide-gray-200 space-y-5\"\u0026gt;\n    \u0026lt;div class=\"mt-5 space-y-5\"\u0026gt;\n      \u0026lt;div class=\"grid grid-cols-3 gap-4 items-start border-t border-gray-200 pt-5\"\u0026gt;\n        \u0026lt;label  class=\"block text-sm font-medium text-gray-700 mt-px pt-2\"\u0026gt;Data File (CSV):\u0026lt;/label\u0026gt;\n        \u0026lt;div class=\"mt-0 col-span-2\" \n            data-controller='uploads' \n            data-action='direct-upload:initialize@window-\u0026gt;uploads#initializeDirectUpload  direct-upload:progress@window-\u0026gt;uploads#progressDirectUpload' \u0026gt;\n          \u0026lt;div class=\"max-w-lg flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md\"\u0026gt;\n            \u0026lt;div class=\"space-y-1 text-center\"\u0026gt;\n              \u0026lt;div class=\"w-full flex text-sm text-gray-600\"\u0026gt;\n                \u0026lt;label for=\"file\" class=\"relative cursor-pointer bg-white rounded-md font-medium text-blue-700 hover:text-blue-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-blue-500\"\u0026gt;\n                  \u0026lt;%= @blob.filename %\u0026gt;\n                \u0026lt;/label\u0026gt;\n              \u0026lt;/div\u0026gt;\n              \u0026lt;div class=\"w-full pt-1\"\u0026gt;\n                \u0026lt;span\u0026gt;Processing file\u0026lt;/span\u0026gt;\n                \u0026lt;div class=\"overflow-hidden w-60 h-4 text-xs flex rounded bg-blue-200\"\u0026gt;\n                  \u0026lt;%= render partial: \"blob_progress\", locals: { blob: @blob, progress: 0 } %\u0026gt;\n                \u0026lt;/div\u0026gt;\n              \u0026lt;/div\u0026gt;\n            \u0026lt;/div\u0026gt;\n          \u0026lt;/div\u0026gt;\n        \u0026lt;/div\u0026gt;\n      \u0026lt;/div\u0026gt;\n      \u0026lt;div class=\"pt-5\"\u0026gt;\n        \u0026lt;div class=\"flex justify-end\"\u0026gt;\n          \u0026lt;%= link_to \"View Results\", books_path, class: \"inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500\" %\u0026gt;\n        \u0026lt;/div\u0026gt;\n      \u0026lt;/div\u0026gt;\n    \u0026lt;/div\u0026gt;\n  \u0026lt;/div\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe’re removing the form, but keeping the layout similar to what’s replaced. It uses the \u003ccode\u003eturbo_stream_from\u003c/code\u003e tag to listen on the blob’s key. This gets around the fact that \u003ccode\u003eActiveStorage::Blob\u003c/code\u003e has a different result for the identify method that the TurboStream class uses to create the channel. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFinally, the ImportBooksJob is queued in the \u003ccode\u003ecreate\u003c/code\u003e action:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eclass ImportController \u0026lt; ApplicationController\n  def show\n  end\n\n  def create\n    @blob = ActiveStorage::Blob.find_signed params[:file]\n    ImportBooksJob.perform_later @blob\n    respond_to do |format|\n      format.turbo_stream\n    end\n  end\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eProcessing a file\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn this sample, we’re using a CSV and the processing will be in the background job \u003ccode\u003eImportBooks\u003c/code\u003e. The job will download the file from file storage, use Ruby’s CSV library to read each row, and then update or insert records depending on what the file contains. One technique I’ve used is to farm out each record into its job, so that if any errors occur, they don’t stop the entire import, but We’ll keep this simple for now.  The progress will be reported back to the front end through the TurboStream connection.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLet’s create a little template that we’ll use to render the progress, \u003ccode\u003eapp/views/application/_blob_progress.html.erb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div id=\"\u0026lt;%= dom_id blob %\u0026gt;\" class=\"rounded animate-pulse shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-blue-800\" style=\"width: \u0026lt;%= progress %\u0026gt;%\" \u0026gt;\u0026lt;/div\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis takes in the blob and the amount of progress as local variables.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003eImportBooksJob\u003c/code\u003e is passed the blob, downloads the file, and parses the CSV. After each iteration, it sends the progress to the front end, and we get a nice loading bar on the page.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003erequire 'csv'\n\nclass ImportBooksJob \u0026lt; ApplicationJob\n  queue_as :default\n\n  def perform(blob)\n    blob.open do |file|\n       csv = CSV.read(file, headers: true)\n       row_count = CSV.read(file, headers: true).size\n       csv.each_with_index do |row, progress|\n        Turbo::StreamsChannel.broadcast_replace_to blob.key, \n                                                  target: blob, partial: \"blob_progress\", \n                                                  locals: { blob: blob, progress: ((progress.to_f / row_count) * 100) }\n       end\n    end\n  end\nend\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003eTurbo::StreamsChannel\u003c/code\u003e can broadcast to the front end. The first parameter, \u003ccode\u003eblob.key\u003c/code\u003e which matches what we are listening for on the page.  The target matches the DOM id on the page, which is the progress indicator, and the partial and locals named parameters are considered the rendering block. This all gets wrapped into a template, and replaced on the front page.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003ePassive yet Interactive\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:video {\"id\":823} --\u003e\n\u003cfigure class=\"wp-block-video\"\u003e\u003cvideo controls=\"\" src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAzMSwicHVyIjoiYmxvYl9pZCJ9fQ==--819c46095e876fdd1af2c2819972b4bb4a66c317/04-HOTWire-Background-Job-Loading-1.mp4\"\u003e\u003c/video\u003e\u003c/figure\u003e\n\u003c!-- /wp:video --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe user wanted to load up a lot of records into the app. We provided a way to use a CSV to upload all the records, so they wouldn’t have to add each one manually. They get a progress bar for the upload, which is delightful if we have a big file, but it still feels luxurious for our website to tell us what’s going on. Once the files been uploaded, a new progress indicator shows how the record import is going. The whole action by the user was a few clicks, but they feel in control because they know what’s going on the whole time. This is the kind of interactivity uses crave in a web app, and a lot of it comes for free in Rails.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2022/10/31/55-adding-loading-screen-with-turbo","url":"https://onrails.blog/2022/10/31/55-adding-loading-screen-with-turbo","title":"Adding Loading Screen with Turbo","date_published":"2022-10-31T13:57:05Z","date_modified":"2026-06-02T00:41:09Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOne great aspect of Turbo frames is lazy loading. You can use this behavior to quickly load in the shell of a UI, and then lazy load all the data. Adding an animating loading status will help give the feeling of immediacy and “a lot of work is happening in the background” without the frustration that the page is stuck. We don’t even need any extra Javascript, since we can animate the page with CSS.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:embed {\"url\":\"https://youtu.be/lXxFzPU-_uE\",\"type\":\"video\",\"providerNameSlug\":\"youtube\",\"responsive\":true,\"className\":\"wp-embed-aspect-4-3 wp-has-aspect-ratio\"} --\u003e\n\u003cfigure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio\"\u003e\u003cdiv class=\"wp-block-embed__wrapper\"\u003e\nhttps://youtu.be/lXxFzPU-_uE\n\u003c/div\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eFull tutorial\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:embed --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eLoading many books\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis tutorial will start from a previous example, \u003ca href=\"https://onrails.blog/2018/09/25/modeling-more-complex-datasets-in-rails/\"\u003eModeling More Complex Datasets\u003c/a\u003e, which used a Book model. We’ll start from scratch with a fresh Rails app and use TailwindCSS for the style.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ rails new BookData -c tailwind\n$ cd BookData \n$ rails g model Book title:string author:string publisher:string category:string isbn:string dewey_decimal_number:string binding:integer\n$ rails db:migrate\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe’re going to make binding an enum since there are only a few options, so change \u003ccode\u003emodels/book.rb\u003c/code\u003e to this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eclass Book \u0026lt; ApplicationRecord\n  enum binding: [:hardcover, :paperback]\nend\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTo test our system with plenty of books, We will use the Faker gem to create about 2000 fake books.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn our Gemfile:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003egem 'faker'\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen run\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ bundle install\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen we’ll create the books in our \u003ccode\u003edb/seeds.rb\u003c/code\u003e file.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e2_000.times do |i|\n  Book.create!( title: Faker::Book.title, \n\tauthor: Faker::Book.author, \n\tpublisher: Faker::Book.publisher,\n\tcategory: Faker::Book.genre,\n\tisbn: \"#{Faker::Number.number(digits: 3)}-#{Faker::Number.number(digits: 1)}-#{Faker::Number.number(digits: 2)}-#{Faker::Number.number(digits: 6)}-#{Faker::Number.number(digits: 1)}\",\n\tdewey_decimal_number: \"#{Faker::Number.number(digits: 3)}.#{Faker::Number.number(digits: 3)}\",\n\tbinding: Faker::Number.between(from: 0, to: 1))\n  print '.' if i % 100 == 0\nend\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow we have a lot of books records that we can use.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLet’s build an index page so that we can look at our books.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ rails g controller books index\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis will update\u0026nbsp;\u003ccode\u003eroutes.rb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eRails.application.routes.draw do\n  get 'books/index'\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd we need to update \u003ccode\u003eapp/controllers/books_controller.rb\u003c/code\u003e\u0026nbsp;file with an index action, and we’ll start by loading all the books.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eclass BooksController \u0026lt; ApplicationController\n  def index\n    @books = Book.all\n  end\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAnd finally, a view at\u0026nbsp;\u003ccode\u003eapp/views/books/index.html.erb\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div\u0026gt;\n  \u0026lt;h1 class=\"font-bold text-4xl\"\u0026gt;All Books\u0026lt;/h1\u0026gt;\n  \u0026lt;table\u0026gt;\n    \u0026lt;thead\u0026gt;\n      \u0026lt;tr\u0026gt;\n        \u0026lt;th\u0026gt;Title\u0026lt;/th\u0026gt;\n        \u0026lt;th\u0026gt;Author\u0026lt;/th\u0026gt;\n        \u0026lt;th\u0026gt;Publisher\u0026lt;/th\u0026gt;\n        \u0026lt;th\u0026gt;Category\u0026lt;/th\u0026gt;\n      \u0026lt;/tr\u0026gt;\n    \u0026lt;/thead\u0026gt;\n    \u0026lt;tbody\u0026gt;\n      \u0026lt;% @books.each do |book| %\u0026gt;\n        \u0026lt;tr\u0026gt;\n          \u0026lt;td\u0026gt;\u0026lt;%= book.title %\u0026gt;\u0026lt;/td\u0026gt;\n          \u0026lt;td\u0026gt;\u0026lt;%= book.author %\u0026gt;\u0026lt;/td\u0026gt;\n          \u0026lt;td\u0026gt;\u0026lt;%= book.publisher %\u0026gt;\u0026lt;/td\u0026gt;\n          \u0026lt;td\u0026gt;\u0026lt;%= book.category %\u0026gt;\u0026lt;/td\u0026gt;\n        \u0026lt;/tr\u0026gt;\n      \u0026lt;% end %\u0026gt;\n    \u0026lt;/tbody\u0026gt;\n  \u0026lt;/table\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eRun\u0026nbsp;\u003ccode\u003e./bin/dev\u003c/code\u003e\u0026nbsp;and visit it in the browser.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"id\":770,\"linkDestination\":\"none\",\"align\":\"center\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAzMCwicHVyIjoiYmxvYl9pZCJ9fQ==--5631c1d606b05617b20d21e30b4595119618023e/DraggedImage.png\" alt=\"\" class=\"wp-image-770\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eLazy Loading \u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe URL path \u003ccode\u003e/books/index\u003c/code\u003e is a little awkward. It would be great if it would load when the site loads up. Let’s add a root controller that will load the books page:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ rails g controller root index\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eChange the \u003ccode\u003eroutes.rb\u003c/code\u003e file to load at the root page:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eRails.application.routes.draw do\n  get 'books/index'\n  root \"root#index\"\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAdd a \u003ccode\u003eturbo-frame\u003c/code\u003e element to the \u003ccode\u003eroot/index.html.erb\u003c/code\u003e page:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div\u0026gt;\n  \u0026lt;h1 class=\"font-bold text-4xl\"\u0026gt;Root Page\u0026lt;/h1\u0026gt;\n  \u0026lt;%= turbo_frame_tag \"books\", src: books_index_path %\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf you don’t change the book’s index page, this will load all the books in, and overwrite the URL. We want to hide the \u003ccode\u003e/books/index\u003c/code\u003e path, so let’s wrap the table in \u003ccode\u003eviews/books/index.html.erb\u003c/code\u003e in a \u003ccode\u003eturbo_frame_tag\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div\u0026gt;\n  \u0026lt;h1 class=\"font-bold text-4xl\"\u0026gt;All Books\u0026lt;/h1\u0026gt;\n  \u0026lt;%= turbo_frame_tag \"books\" do %\u0026gt;\n    \u0026lt;table\u0026gt;\n      \u0026lt;thead\u0026gt;\n        \u0026lt;tr\u0026gt;\n          \u0026lt;th\u0026gt;Title\u0026lt;/th\u0026gt;\n          \u0026lt;th\u0026gt;Author\u0026lt;/th\u0026gt;\n          \u0026lt;th\u0026gt;Publisher\u0026lt;/th\u0026gt;\n          \u0026lt;th\u0026gt;Category\u0026lt;/th\u0026gt;\n        \u0026lt;/tr\u0026gt;\n      \u0026lt;/thead\u0026gt;\n      \u0026lt;tbody\u0026gt;\n        \u0026lt;% @books.each do |book| %\u0026gt;\n          \u0026lt;tr\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;%= book.title %\u0026gt;\u0026lt;/td\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;%= book.author %\u0026gt;\u0026lt;/td\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;%= book.publisher %\u0026gt;\u0026lt;/td\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;%= book.category %\u0026gt;\u0026lt;/td\u0026gt;\n          \u0026lt;/tr\u0026gt;\n        \u0026lt;% end %\u0026gt;\n      \u0026lt;/tbody\u0026gt;\n    \u0026lt;/table\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, when you load the root page, there is a skeleton that loads in the books table.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"id\":769,\"linkDestination\":\"none\",\"align\":\"center\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyNywicHVyIjoiYmxvYl9pZCJ9fQ==--5ac2c0d8e2a7d15084c99bb0921efb88040c0224/03.1-HOTWire-Lazy-Loading-Tables.gif\" alt=\"\" class=\"wp-image-769\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAnimate the loading\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLoading all these books could take a while. This could stand in for a different page with more complicated database calls that could take a few seconds. Let’s put in a small skeleton, animate a shimmering effect, and give the appearance that the page is working in the background. Look at the revised \u003ccode\u003e/views/root/index.html.erb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div\u0026gt;\n  \u0026lt;h1 class=\"font-bold text-4xl\"\u0026gt;Root Page\u0026lt;/h1\u0026gt;\n  \u0026lt;%= turbo_frame_tag \"books\", src: books_index_path do %\u0026gt;\n    \u0026lt;table class=\"animate-pulse\"\u0026gt;\n      \u0026lt;thead\u0026gt;\n        \u0026lt;tr\u0026gt;\n          \u0026lt;th\u0026gt;Title\u0026lt;/th\u0026gt;\n          \u0026lt;th\u0026gt;Author\u0026lt;/th\u0026gt;\n          \u0026lt;th\u0026gt;Publisher\u0026lt;/th\u0026gt;\n          \u0026lt;th\u0026gt;Category\u0026lt;/th\u0026gt;\n        \u0026lt;/tr\u0026gt;\n      \u0026lt;/thead\u0026gt;\n      \u0026lt;tbody\u0026gt;\n        \u0026lt;% (1..10).each do %\u0026gt;\n          \u0026lt;tr\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;div class=\"bg-slate-200 h-8 rounded w-64\"\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;/td\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;div class=\"bg-slate-200 h-8 rounded w-64\"\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;/td\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;div class=\"bg-slate-200 h-8 rounded w-64\"\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;/td\u0026gt;\n            \u0026lt;td\u0026gt;\u0026lt;div class=\"bg-slate-200 h-8 rounded w-64\"\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;/td\u0026gt;\n          \u0026lt;/tr\u0026gt;\n        \u0026lt;% end %\u0026gt;\n      \u0026lt;/tbody\u0026gt;\n    \u0026lt;/table\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe turbo-frame now has a block that puts in a skeleton table. It uses the tailwind animate-pulse class, which is a pulsing CSS animation. Instead of iterating through book records, it generates 10 rows. Each \u003ccode\u003etd\u003c/code\u003e cell has a rounded div that acts a visual placeholder. Loading the page shows a pleasant placeholder while all the books come in over the wire.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"id\":768,\"linkDestination\":\"none\",\"align\":\"center\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyOCwicHVyIjoiYmxvYl9pZCJ9fQ==--3fe80db07935be3ffd67668654a73cd8e1f3f0eb/03.2-HOTWire-Lazy-Loading-Tables-with-animation.gif\" alt=\"\" class=\"wp-image-768\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eFree Interactivity\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis shows how we can use a lazy loading page to add interactivity to our page with little extra work, and no extra JavaScript. You could imagine having to see up a Stimulus controller that loaded data in when it connected. Turbo allows us to skip even that step, and just use basic HTML and an extra Rails controller. You could even build a dashboard that loaded data from several sources.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2022/09/19/hotwire-notifications","url":"https://onrails.blog/2022/09/19/hotwire-notifications","title":"Interactive HOTWire Notifications with Turbo Streams","date_published":"2022-09-19T10:00:00Z","date_modified":"2026-06-02T00:41:11Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eRails’ use of \u003ccode\u003eflash\u003c/code\u003e messages is a great way to provide context for customer actions. If they delete an object, flashing a notice that the delete action succeeded, or perhaps failed, gives them more context to make the next decision. With HOTWire’s asynchronous nature, you don’t get those notifications in the same way, especially if you’re just replacing a part of a page.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eRefactoring Notifications\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLet’s refactor the flash messages into the upper-left corner of the app, where they’ll float. Using a TailwindUI design, the key component is something with an \u003ccode\u003eid\u003c/code\u003e where Turbo can append the notification. I’m using a \u003ccode\u003ediv\u003c/code\u003e with the \u003ccode\u003eid=\"notifications\"\u003c/code\u003e. I refactored the HTML for an alert or a notice into a partial, which I put in the \u003ccode\u003eapp/views/layouts\u003c/code\u003e folder as \u003ccode\u003e_alert.html.erb\u003c/code\u003e and \u003ccode\u003e_notice.html.erb\u003c/code\u003e. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eDisplaying Alerts from TurboStreams\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn any \u003ccode\u003e*.turbo_stream.erb\u003c/code\u003e view, add the following to check if there is an alert or notification:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;% if notice %\u0026gt;\n  \u0026lt;%= turbo_stream.append 'notifications' do %\u0026gt;\n    \u0026lt;%= render 'layouts/notice' %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\n\u0026lt;% if alert %\u0026gt;\n  \u0026lt;%= turbo_stream.append 'notifications' do %\u0026gt;\n    \u0026lt;%= render 'layouts/alert' %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis will append the notification to the notifications' element, and display the notification.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eBuilding the notifications\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eYou can start with a new project, and scaffold a Post model:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ rails new notification_test -c tailwind\n$ rails g scaffold Post title:string body:text\n$ rails db:migrate\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWe’re going to add the HTML code to display our notifications. In \u003ccode\u003eapp/views/layout/application.html.erb\u003c/code\u003e, add the notifications “home”, which won’t show anything, but Turbo will append notification onto it.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;body\u0026gt;\n  \u0026lt;main class=\"container mx-auto mt-28 px-5 flex\"\u0026gt;\n    \u0026lt;div aria-live=\"assertive\" class=\"fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6 sm:items-start z-50\"\u0026gt;\n      \u0026lt;div id=\"notifications\" class=\"w-full flex flex-col items-center space-y-4 sm:items-end\"\u0026gt;\n        \u0026lt;% if notice %\u0026gt;\n          \u0026lt;%= render 'layouts/notice' %\u0026gt;\n        \u0026lt;% end %\u0026gt;\n        \u0026lt;% if alert %\u0026gt;\n          \u0026lt;%= render 'layouts/alert' %\u0026gt;\n        \u0026lt;% end %\u0026gt;\n      \u0026lt;/div\u0026gt;\n    \u0026lt;/div\u0026gt;\n    \u0026lt;%= yield %\u0026gt;\n  \u0026lt;/main\u0026gt;\n\u0026lt;/body\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the \u003ccode\u003eapp/views/layouts\u003c/code\u003e folder, add two almost identical partials that will display the alert or notice. Here is \u003ccode\u003eapp/views/layout/_alert.html.erb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div data-controller=\"alert\"\n  data-transition-enter=\"transform ease-out duration-300 transition\"\n  data-transition-enter-start=\"translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2\"\n  data-transition-enter-end=\"translate-y-0 opacity-100 sm:translate-x-0\"\n  data-transition-leave=\"transition ease-in duration-100\"\n  data-transition-leave-start=\"opacity-100\"\n  data-transition-leave-end=\"opacity-0\"\n  class=\"max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden hidden\"\u0026gt;\n  \u0026lt;div class=\"p-4\"\u0026gt;\n    \u0026lt;div class=\"flex items-start\"\u0026gt;\n      \u0026lt;div class=\"flex-shrink-0\"\u0026gt;\n        \u0026lt;svg class=\"h-6 w-6 text-red-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"\u0026gt;\n          \u0026lt;path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" /\u0026gt;\n        \u0026lt;/svg\u0026gt;\n      \u0026lt;/div\u0026gt;\n      \u0026lt;div class=\"ml-3 w-0 flex-1 pt-0.5\"\u0026gt;\n        \u0026lt;p class=\"text-sm font-medium text-gray-900\"\u0026gt;Alert!\u0026lt;/p\u0026gt;\n        \u0026lt;p class=\"mt-1 text-sm text-gray-500\"\u0026gt;\u0026lt;%= alert %\u0026gt;\u0026lt;/p\u0026gt;\n\t\t\u0026lt;%# Need to clear flash[:alert] once displayed %\u0026gt;\n        \u0026lt;% flash[:alert] = nil %\u0026gt;\n      \u0026lt;/div\u0026gt;\n      \u0026lt;div class=\"ml-4 flex-shrink-0 flex\"\u0026gt;\n        \u0026lt;button type=\"button\" data-action=\"alert#close\"  class=\"bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\"\u0026gt;\n          \u0026lt;span class=\"sr-only\"\u0026gt;Close\u0026lt;/span\u0026gt;\n          \u0026lt;!-- Heroicon name: solid/x --\u0026gt;\n          \u0026lt;svg class=\"h-5 w-5\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\"\u0026gt;\n            \u0026lt;path fill-rule=\"evenodd\" d=\"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /\u0026gt;\n          \u0026lt;/svg\u0026gt;\n        \u0026lt;/button\u0026gt;\n      \u0026lt;/div\u0026gt;\n    \u0026lt;/div\u0026gt;\n  \u0026lt;/div\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHere is the notice in \u003ccode\u003eapp/views/layout/_notice.html.erb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div data-controller=\"alert\"\n  data-transition-enter=\"transform ease-out duration-300 transition\"\n  data-transition-enter-start=\"translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2\"\n  data-transition-enter-end=\"translate-y-0 opacity-100 sm:translate-x-0\"\n  data-transition-leave=\"transition ease-in duration-100\"\n  data-transition-leave-start=\"opacity-100\"\n  data-transition-leave-end=\"opacity-0\"\n  class=\"max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden hidden\"\u0026gt;\n  \u0026lt;div class=\"p-4\"\u0026gt;\n    \u0026lt;div class=\"flex items-start\"\u0026gt;\n      \u0026lt;div class=\"flex-shrink-0\"\u0026gt;\n        \u0026lt;!-- Heroicon name: outline/check-circle --\u0026gt;\n        \u0026lt;svg class=\"h-6 w-6 text-green-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" aria-hidden=\"true\"\u0026gt;\n          \u0026lt;path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\" /\u0026gt;\n        \u0026lt;/svg\u0026gt;\n      \u0026lt;/div\u0026gt;\n      \u0026lt;div class=\"ml-3 w-0 flex-1 pt-0.5\"\u0026gt;\n        \u0026lt;p class=\"text-sm font-medium text-gray-900\"\u0026gt;Notice!\u0026lt;/p\u0026gt;\n        \u0026lt;p class=\"mt-1 text-sm text-gray-500\"\u0026gt;\u0026lt;%= notice %\u0026gt;\u0026lt;/p\u0026gt;\n      \t\u0026lt;%# Need to clear flash[:notice] once displayed %\u0026gt;\n        \u0026lt;% flash[:notice] = nil %\u0026gt;\n\t  \u0026lt;/div\u0026gt;\n      \u0026lt;div class=\"ml-4 flex-shrink-0 flex\"\u0026gt;\n        \u0026lt;button type=\"button\" data-action=\"alert#close\"  class=\"bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\"\u0026gt;\n          \u0026lt;span class=\"sr-only\"\u0026gt;Close\u0026lt;/span\u0026gt;\n          \u0026lt;!-- Heroicon name: solid/x --\u0026gt;\n          \u0026lt;svg class=\"h-5 w-5\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\"\u0026gt;\n            \u0026lt;path fill-rule=\"evenodd\" d=\"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z\" clip-rule=\"evenodd\" /\u0026gt;\n          \u0026lt;/svg\u0026gt;\n        \u0026lt;/button\u0026gt;\n      \u0026lt;/div\u0026gt;\n    \u0026lt;/div\u0026gt;\n  \u0026lt;/div\u0026gt;\n\u0026lt;/div\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, when you perform an action, like creating a Post, you’ll see the notification floating above.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"align\":\"center\",\"id\":751,\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyNSwicHVyIjoiYmxvYl9pZCJ9fQ==--607f5b0439e5ed356691e826ff8f0356f25c5bab/DraggedImage-2.png\" alt=\"\" class=\"wp-image-751\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHopefully, you noticed the Stimulus alert controller that will make this floating notification disappear after appearing, or allow the user to click the X to remove the notification. This uses the \u003ca href=\"https://github.com/mmccall10/el-transition\"\u003e\u003ccode\u003eel-transition\u003c/code\u003e\u003c/a\u003e library to manage the animations. Add it with your current JavaScript management tool, for example with importmap:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ ./bin/importmap pin el-transition\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eor yarn:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ yarn add el-transition\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAdd the Stimulus controller, \u003ccode\u003ealert_controller.js\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eimport { Controller } from \"@hotwired/stimulus\";\nimport { enter, leave } from \"el-transition\";\n\nlet TIMEOUT_MILLISECONDS = 2000;\n\nexport default class extends Controller {\n  connect() {\n    enter(this.element).then(() =\u0026gt; {\n      setTimeout(() =\u0026gt; {\n        this.close();\n      }, TIMEOUT_MILLISECONDS);\n    });\n  }\n\n  close() {\n    leave(this.element).then(() =\u0026gt; {\n      this.element.remove();\n    });\n  }\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow we have a nice floating notification layout, with a Stimulus controller providing the entering and leaving animations, and a timeout that removes the notification after a few seconds. Let’s add the Turbo Stream magic.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eTurboFrames and TurboStreams\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, on the \u003ccode\u003eposts/index.html.erb\u003c/code\u003e page, let’s wrap the “New Post” button in a \u003ccode\u003eturbo_frame_tag\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;%= turbo_frame_tag 'new_post' do %\u0026gt;\n  \u0026lt;%= link_to 'New post', new_post_path, class: \"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium\" %\u0026gt;\n\u0026lt;% end %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the \u003ccode\u003eposts/new.html.erb\u003c/code\u003e, wrap the key part of the form in the same turbo frame. This will be styled to be a modal that appears in the middle of the page:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;%= turbo_frame_tag 'new_post' do %\u0026gt;\n  \u0026lt;div class=\"fixed inset-0 z-10 overflow-y-auto\"\u0026gt;\n    \u0026lt;div class=\"flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0\"\u0026gt;\n      \u0026lt;div class=\"relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6\"\u0026gt;\n        \u0026lt;h1 class=\"font-bold text-4xl\"\u0026gt;New post\u0026lt;/h1\u0026gt;\n        \u0026lt;%= render \"form\", post: @post %\u0026gt;\n      \u0026lt;/div\u0026gt;\n    \u0026lt;/div\u0026gt;\n  \u0026lt;/div\u0026gt;\n\u0026lt;% end %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTurbo is going to handle the replacement, so the page will load the new form in the middle of the page.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"align\":\"center\",\"id\":750,\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyNCwicHVyIjoiYmxvYl9pZCJ9fQ==--696ceca2bb83a3aa62a534b7a69d8a2e6ce800d5/DraggedImage-1.png\" alt=\"\" class=\"wp-image-750\"\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhen the “Create Post” button is clicked, a request goes to Posts controller on the \u003ccode\u003ecreate\u003c/code\u003e action. Add a new format response to the create action, complete with the flash notices:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e  # POST /posts or /posts.json\n  def create\n    @post = Post.new(post_params)\n\n    respond_to do |format|\n      if @post.save\n        format.turbo_stream { flash[:notice] = 'Post was successfully created.' }\n        format.json { render :show, status: :created, location: @post }\n      else\n        format.turbo_stream { flash[:alert] = 'Post was not created.' }\n        format.json { render json: @post.errors, status: :unprocessable_entity }\n      end\n    end\n  end\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLet’s add the \u003ccode\u003eposts/create.turbo_stream.erb\u003c/code\u003e template that will append the new post to the page, replace the “New Post” button, and append our notice to the notifications' element.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;%= turbo_stream.append 'posts' do %\u0026gt;\n  \u0026lt;%= render @post %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\n\u0026lt;%= turbo_stream.replace 'new_post' do %\u0026gt;\n  \u0026lt;%= turbo_frame_tag 'new_post' do %\u0026gt;\n    \u0026lt;%= link_to 'New post', new_post_path, class: \"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium\" %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\n\u0026lt;% if notice %\u0026gt;\n  \u0026lt;%= turbo_stream.append 'notifications' do %\u0026gt;\n    \u0026lt;%= render 'layouts/notice' %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\n\u0026lt;% if alert %\u0026gt;\n  \u0026lt;%= turbo_stream.append 'notifications' do %\u0026gt;\n    \u0026lt;%= render 'layouts/alert' %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, we can create a post, and get the SPA look, without having to resort to rendering everything from JavaScript. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"id\":755,\"sizeSlug\":\"full\",\"linkDestination\":\"media\"} --\u003e\n\u003cfigure class=\"wp-block-image size-full\"\u003e\u003ca href=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyMiwicHVyIjoiYmxvYl9pZCJ9fQ==--8f436ee14383f388eaaec0881e0d2c201dbbb0e0/02-Hotwire-Turbo-Notifications.gif\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyMiwicHVyIjoiYmxvYl9pZCJ9fQ==--8f436ee14383f388eaaec0881e0d2c201dbbb0e0/02-Hotwire-Turbo-Notifications.gif\" alt=\"\" class=\"wp-image-755\"\u003e\u003c/a\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eThe whole notification look\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIt’s HTML Over The Wire at its best.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2022/09/13/stylizing-actionmailers-with-maizzle","url":"https://onrails.blog/2022/09/13/stylizing-actionmailers-with-maizzle","title":"Stylizing ActionMailers with Maizzle","date_published":"2022-09-13T14:58:35Z","date_modified":"2026-06-02T00:41:09Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI’ve been using Tailwind CSS for all my new projects, and I’ve been migrating my other projects over slowly. With Tailwind’s Rails gem, it’s been incredibly easy to drop in Tailwind, include my old CSS, and then move pages over. Tailwind is incredibly deferential to other CSS frameworks because it has a unique utility system, rather than trying to redefine the \u003ccode\u003e.button\u003c/code\u003e class yet again. Maizzle is a tool, written in Javascript, that takes Tailwind styled HTML for a mailer, and generates hardcoded styles. It optimizes for inboxes, so it makes some default decisions, such as column width. If you have an existing theme in your app, you can easily customize all your mailers to match your website’s theme and design.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eHow I’m using it\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI purchased one of the theme packs from the \u003ca href=\"https://craftingemails.com/email-templates\"\u003ecraftingemails\u003c/a\u003e as a great starting point. One nice thing about Maizzle is that you can bring your CSS, and it will add the styles so that it looks correct in as many email clients as possible.  I added my theme colors to make the templates match my project. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eSetting it up\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf you’ve already set up Rails on your machine, with Tailwind CSS, you should have the correct version of node setup. You can follow the directions at \u003ca href=\"https://maizzle.com/docs/introduction\"\u003ehttps://maizzle.com/docs/introduction\u003c/a\u003e, but here’s what I did.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, download the \u003ca href=\"https://craftingemails.com/email-templates\"\u003eemail template\u003c/a\u003e or the \u003ca href=\"https://github.com/maizzle/maizzle\"\u003estarter version from GitHub\u003c/a\u003e. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen, install maizzle and the dependencies:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ npm install\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen, start the development server, which watches the source folder, and builds them.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ maizzle serve\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe one change necessary is to change the \u003ccode\u003ebaseURL\u003c/code\u003e in the \u003ccode\u003econfig.production.js\u003c/code\u003e file. Set it to an empty string so that nothing is prepended for image tags or links.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003emodule.exports = {\n  baseURL: \"\",\n  // ... other options\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eDesigning the templates\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eSince Maizzle just uses CSS, we can take a basic HTML template, and make it look great. This means you’ll want to change the \u003ccode\u003eviews/layouts/mailer.html.erb\u003c/code\u003e to get rid of all the boilerplate to just 1 line:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;%= yield %\u0026gt; \u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAll the header and HTML tags will be in each email template.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhen designing the template in the maizzle folder, you’ll need to create dummy data to fill in. When you want to replace the data in the actual template that you’ll copy into your Rails app, you could manually add the ERB tags. You could also use Maizzle’s built-in preprocessors to use dummy data for development and actual ERB tags in the production build. For example, the Maizzle starter on GitHub has a transactional template that includes the logo. In Rails, we’ll want to use the \u003ccode\u003easset_path\u003c/code\u003e to the icon instead of a local path. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHere is the icon:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e \u0026lt;img src=\"images/maizzle.png\" width=\"70\" alt=\"Maizzle\" class=\"max-w-full align-middle [border:0]\"\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf we test the \u003ccode\u003epage.env\u003c/code\u003e value, we can select the right tags:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;if condition=\"page.env === 'production'\"\u0026gt;\n     \u0026lt;img src=\"\u0026lt;%= asset_path('maizzle.png') %\u0026gt;\" width=\"70\" alt=\"Maizzle\" class=\"max-w-full align-middle [border:0]\"\u0026gt;\n\u0026lt;/if\u0026gt;\n\u0026lt;if condition=\"page.env !== 'production'\"\u0026gt;\n     \u0026lt;img src=\"images/maizzle.png\" width=\"70\" alt=\"Maizzle\" class=\"max-w-full align-middle [border:0]\"\u0026gt;\n\u0026lt;/if\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, you can see the image when you’re designing the template, but when it’s built for distribution, it includes the ERB tag, so you can copy and paste it into your mailer’s view, without have to tweak anything.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOccasionally, you don’t mind the ERB tags in the design, so you can just add them and know they’ll be converted on the Rails side.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:image {\"align\":\"center\",\"id\":739,\"linkDestination\":\"none\"} --\u003e\n\u003cfigure class=\"wp-block-image aligncenter\"\u003e\u003cimg src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyNiwicHVyIjoiYmxvYl9pZCJ9fQ==--f63dbb33197e49a171031b34059d45144bb2caec/DraggedImage.png\" alt=\"Example of ERB tags in the output during design\" class=\"wp-image-739\"\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eExample of ERB tags in the output during design\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:image --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eMove Into Rails\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOnce you’ve got your design working, you should run the build command:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e$ maizzle build production\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis will optimize the CSS in the templates, and perform any switching we needed for the Rails side. \u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLook for the template in the \u003ccode\u003edist\u003c/code\u003e folder. Copy all the HTML from the template, and the preview the email with your app’s \u003ca href=\"https://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails\"\u003eRails mailer previews\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eFancy Email Templates\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, you have prettier looking email templates generated with your Tailwind theme. This should make all your transactional emails feel on brand when you correspond with your customers.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2022/06/24/stimulus-js-hotwire-tutorial-interactive-deletes","url":"https://onrails.blog/2022/06/24/stimulus-js-hotwire-tutorial-interactive-deletes","title":"Stimulus.js \u0026 HOTWire Tutorial: Interactive Deletes","date_published":"2022-06-24T10:48:35Z","date_modified":"2026-06-02T00:41:06Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eInteractive websites have that feeling of immediacy. Clicked links respond in milliseconds, and there is never a need to \u003cem\u003ewait\u003c/em\u003e… Waiting for a remote record to delete, and then the whole page refresh afterwards can feel like an eternity in web’s modernity. But a little Stimulus works well with Turbo to make items on a page disappear immediately, all while a network request happens in the background. This tutorial takes a remote deletion link, adds an in-place confirmation message, and then hides the element when the delete request is sent to the server. When the request comes back with a success, the element is removed from the page, or with an error, the element put right back in place.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe controller is going to listen for \u003ca href=\"https://turbo.hotwired.dev/reference/events\"\u003ea few different events emitted by Turbo\u003c/a\u003e\u003csup\u003e\u003ca id=\"ffn1\" href=\"#fn1\"\u003e1\u003c/a\u003e\u003c/sup\u003e when the delete link is clicked. It stops the remote request the first time the link is clicked, and puts a confirmation message in the link. This feels better than the usual full alert modal that adding confirmation message usually throws up. Clicking the link a second time lets the request go back to the server. The element’s style is then set to \u003ccode\u003edisplay:none;\u003c/code\u003e so that it disappears immediately. A successful Turbo Stream response removes the element entirely, and an unsuccessful response reverts the style, so the element appears again. A timeout is added after the first click to reset the state of the controller and the link if it isn’t clicked a second time.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eThe HTML\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI’m working with a simple ActiveRecord \u003ccode\u003ePost\u003c/code\u003e model which has a title, author, and text. The controller is similar to what you could generate from the Rails scaffold command. On the \u003ccode\u003eindex.html.erb\u003c/code\u003e view, this is the HTML:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e  \u0026lt;h1 \u0026gt;All Posts\u0026lt;/h1\u0026gt;\n  \u0026lt;%= link_to \"New Post\", new_post_path %\u0026gt;\n  \u0026lt;% @posts.each do |post| %\u0026gt;\n    \u0026lt;div id=\"\u0026lt;%= dom_id post %\u0026gt;\"\n        data-controller=\"delete\"\n        data-delete-message-value=\"\u0026lt;strong\u0026gt;Are you sure?\u0026lt;/strong\u0026gt;\"\u0026gt;\n      \u0026lt;p\u0026gt;\n        \u0026lt;h2\u0026gt;\u0026lt;%= link_to post.title, post %\u0026gt;\u0026lt;/h2\u0026gt;\n        \u0026lt;strong\u0026gt;\u0026lt;%= post.author %\u0026gt;\u0026lt;/strong\u0026gt;\n        \u0026lt;br /\u0026gt;\n        \u0026lt;em\u0026gt;\u0026lt;%= post.created_at.strftime(\"%l:%M %P\") %\u0026gt;\u0026lt;/em\u0026gt;\n        \u0026lt;br /\u0026gt;\n        \u0026lt;%= link_to \"Delete\", post_path(post), data: { 'turbo-method': \"delete\", 'delete-target': \"link\", action: \"turbo:click-\u0026gt;delete#captureClick turbo:before-fetch-request@window-\u0026gt;delete#deleteAndHide\" } %\u0026gt;\n      \u0026lt;/p\u0026gt;\n    \u0026lt;/div\u0026gt;\n  \u0026lt;% end %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003cbr\u003eThere is a \u003ccode\u003ediv\u003c/code\u003e element for the delete controller. The delete link towards the bottom is a target of the delete controller, since the controller needs to use it when getting click events and manipulate the message on it. The controller listens for the \u003ccode\u003eturbo:click\u003c/code\u003e and the \u003ccode\u003eturbo:before-fetch-request\u003c/code\u003e events so that the controller can add our visual interactivity. The controller doesn’t need to change anything else with how Rails sends and receives the delete request, meaning less work for the Stimulus controller and fewer chances for bugs.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eThe Stimulus Controller\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003edelete_controller.js\u003c/code\u003e Stimulus controller is the component in charge of the delete actions. It keeps track of the click state, changes the link text, and handles the result of the server’s response.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOn \u003ccode\u003econnect()\u003c/code\u003e, the controller sets its delete state to \u003ccode\u003efalse\u003c/code\u003e. This dictates later on how the click handler behaves. It sets a value called \u003ccode\u003eclickedController\u003c/code\u003e to \u003ccode\u003efalse\u003c/code\u003e. This is used to keep state between the \u003ccode\u003eturbo:click\u003c/code\u003e event and the \u003ccode\u003eturbo:before-fetch-request\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn Turbo, the click event allows Javascript to cancel a Turbo request, and have the browser send the request normally. The controller uses it know that a click has happened, because the the \u003ccode\u003eturbo:before-fetch-request\u003c/code\u003e event is called on the document, and that’s the event similar to the UJS \u003ccode\u003eajax:success\u003c/code\u003e event that the controller will use to cancel the request and change the confirmation message.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOn a call to \u003ccode\u003edeleteAndHide(event)\u003c/code\u003e when delete is false, the controller records the current delete link text, changes the delete link to a new confirmation message, sets a reset timeout, and then stops the click event. On a call to \u003ccode\u003edeleteAndHide(event)\u003c/code\u003e when delete is true, the controller hides its element, and adds listeners for the server response. The response proceeds back to the server as usual.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003ehandleResponse(event)\u003c/code\u003e method resets the element back to the way the element looked, and makes the element visible again. It doesn’t do anything on success, because Turbo will remove it based on the server response.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003eresetState()\u003c/code\u003e method removes the event listeners, sets the delete link back to the original test, and resets the delete state back to false.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003econst RESET_TIMEOUT_MILLIS = 3000;\n\nimport { Controller } from \"@hotwired/stimulus\";\n\nexport default class extends Controller {\n  static targets = [\"link\"];\n  static values = { message: String };\n\n  connect() {\n    this.delete = false;\n    this.clickedController = false;\n  }\n\n  async captureClick(event) {\n    this.clickedController = this.linkTarget == event.target;\n  }\n\n  async deleteAndHide(event) {\n    if (this.clickedController) {\n      this.clickedController = false;\n      if (this.delete) {\n        this.element.style = \"display: none;\";\n        document.addEventListener(\n          \"turbo:before-fetch-response\",\n          this.handleResponse.bind(this)\n        );\n      } else {\n        this.oldMessage = this.linkTarget.innerHTML;\n        this.linkTarget.innerHTML = this.messageValue;\n        this.delete = true;\n\n        this.timeout = setTimeout(() =\u0026gt; {\n          this.resetState();\n        }, RESET_TIMEOUT_MILLIS);\n        event.preventDefault();\n        return false;\n      }\n    }\n  }\n\n  handleResponse(event) {\n    clearTimeout(this.timeout);\n    this.resetState();\n    console.log(this);\n    console.log(this.element);\n    if (event.detail.fetchResponse.response.status != 204) {\n      this.element.style = \"\";\n    }\n  }\n\n  resetState() {\n    if (this.delete) {\n      document.removeEventListener(\n        \"turbo:before-fetch-response\",\n        this.handleResponse.bind(this)\n      );\n      this.linkTarget.innerHTML = this.oldMessage;\n      this.delete = false;\n    }\n  }\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eRuby Controller\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe delete method on the rails controller needs to respond to the turbo stream format. This is accomplished by adding \u003cstrong\u003e\u003ccode\u003eformat.turbo_stream\u003c/code\u003e\u003c/strong\u003e. It’s using a nifty inline render of the \u003ccode\u003eturbo_stream\u003c/code\u003e remove command, so we don’t need to write an ERB partial.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e  def destroy\n    @post.destroy\n\n    respond_to do |format|\n      format.turbo_stream { render turbo_stream: turbo_stream.remove(@post) }\n    end\n  end\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003ePractice?\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTry to add something to alert the deleter on the page when the deletion fails. It can assume that failure isn’t a likely case, so the error message should be very disruptive to the page flow.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eInteractivity\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eBy making actions on a web page feel immediate, web app users feel more confident that the app registered their wishes. Deletion is definitely a good spot for double checking about someone’s intention, but by changing the delete link text, instead of an alert, it’s not as disruptive as usual. Imagine having to delete hundreds of records, and having to move the cursor multiple times for each action, and you’ll see this method is better for everyone.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eWant To Learn More?\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eTry out some more of my \u003ca href=\"https://johnbeatty.co/stimulus-js-tutorials/\"\u003eStimulus.js Tutorials\u003c/a\u003e. This is an update of previous tutorial that used Rails UJS, \u003ca href=\"https://onrails.blog/2019/02/27/stimulus-js-tutorial-interactive-deletes-with-rails-ujs/\"\u003eStimulus.js Tutorial: Interactive Deletes with Rails UJS\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:list {\"ordered\":true} --\u003e\n\u003col id=\"footnotes\"\u003e\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://turbo.hotwired.dev/reference/events\"\u003ehttps://turbo.hotwired.dev/reference/events\u003c/a\u003e \u003ca href=\"#ffn1\"\u003e↩︎\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\u003c/ol\u003e\n\u003c!-- /wp:list --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2022/06/01/interactive-hotwired-paginated-deletes","url":"https://onrails.blog/2022/06/01/interactive-hotwired-paginated-deletes","title":"Interactive HOTWired paginated deletes","date_published":"2022-06-01T20:16:26Z","date_modified":"2026-06-02T00:41:06Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhen you have a lot of records, it makes sense to paginate the table. That way, instead of loading thousands of records, you can load a subset, with better performance, and allow your user to move through the pages as leisure. It’s also better for usability, because it’s easier to go back to a particular page rather than scroll through a long list.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI like to use \u003ca href=\"https://github.com/basecamp/geared_pagination\"\u003eBasecamp’s \u003ccode\u003egeared_pagination\u003c/code\u003e\u003c/a\u003e gem to manage the pagination, offsets, and current page. I’ve found that when I want to delete a particular record, I could the HOTWire or javascript to remove the individual row. But it wouldn’t update the page counts, or make add in another record. This was a usability problem when I had to delete lots of rows. I would eventually remove all the rows on the page, and then I had to refresh to pull more records from the database.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI realized I can use \u003ca href=\"https://turbo.hotwired.dev/reference/frames\"\u003eTurbo Frames\u003c/a\u003e to replace the entire table, which will update the page counts, and add data back on to the page. Here is how it works below:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:video {\"id\":722} --\u003e\n\u003cfigure class=\"wp-block-video\"\u003e\u003cvideo controls=\"\" src=\"/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTAyMCwicHVyIjoiYmxvYl9pZCJ9fQ==--0fb93a5a1b13da9cd2bba9783cc8f4a6928f2c4c/01-Hotwire-Paginated-Delete.mp4\"\u003e\u003c/video\u003e\u003cfigcaption class=\"wp-element-caption\"\u003eInteractive deletes on a paginated table using HOTWire\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c!-- /wp:video --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHere’s how it all works. This tutorial assumes Rails 7, and HOTWire configured with the \u003ca href=\"https://github.com/hotwired/turbo-rails\"\u003e\u003ccode\u003eturbo-rails\u003c/code\u003e gem\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eThe Controller\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI’m going to use a Book model, but you can really use any of your records. Here is the \u003ccode\u003ebooks_controller.rb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e# frozen_string_literal: true\n\nclass BooksController \u0026lt; ApplicationController\n  def index\n    set_page_and_extract_portion_from Book.all\n  end\n\n  def destroy\n    @book = Book.find params[:id]\n    @book.destroy\n\n    set_page_and_extract_portion_from Book.all\n  end\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003eindex\u003c/code\u003e method will use Geared pagination’s method to extract a selection from all the Book records. The \u003ccode\u003edestroy\u003c/code\u003e method is going to destroy the record, ad you would expect, and then it’s going to call the pagination method again. This will bring up the records we’ll use in the \u003ca href=\"https://turbo.hotwired.dev/reference/streams\"\u003eTurbo Stream\u003c/a\u003e to replace the table.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading {\"level\":1} --\u003e\n\u003ch1 class=\"wp-block-heading\"\u003eThe Views\u003c/h1\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eSince there are two actions in the controller, we’ll use two corresponding \u003ccode\u003eerb\u003c/code\u003e files. First, here is a partial that will be shared between the two actions, \u003ccode\u003e_book_records.html.erb\u003c/code\u003e:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;% unless page.first? %\u0026gt;\n  \u0026lt;%= link_to \"Prev\", books_path(page: page.number - 1 ) %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u0026lt;span\u0026gt;\n  Showing page \u0026lt;%= page.number %\u0026gt; of \u0026lt;%= page.recordset.page_count %\u0026gt;\n  (\u0026lt;%= page.recordset.records_count %\u0026gt; total)\n\u0026lt;/span\u0026gt;\n\u0026lt;% unless page.last? %\u0026gt;\n  \u0026lt;%= link_to \"Next\", books_path(page: page.next_param) %\u0026gt;\n\u0026lt;% end %\u0026gt;\n\u0026lt;table class=\"table\"\u0026gt;\n  \u0026lt;thead\u0026gt;\n    \u0026lt;tr\u0026gt;\n      \u0026lt;th\u0026gt;Title\u0026lt;/th\u0026gt;\n      \u0026lt;th\u0026gt;Author\u0026lt;/th\u0026gt;\n      \u0026lt;th\u0026gt;Publisher\u0026lt;/th\u0026gt;\n      \u0026lt;th\u0026gt;Category\u0026lt;/th\u0026gt;\n      \u0026lt;th\u0026gt;\u0026lt;/th\u0026gt;\n    \u0026lt;/tr\u0026gt;\n  \u0026lt;/thead\u0026gt;\n  \u0026lt;tbody\u0026gt;\n    \u0026lt;% page.records.each do |book| %\u0026gt;\n      \u0026lt;tr\u0026gt;\n        \u0026lt;td\u0026gt;\u0026lt;%= book.title %\u0026gt;\u0026lt;/td\u0026gt;\n        \u0026lt;td\u0026gt;\u0026lt;%= book.author %\u0026gt;\u0026lt;/td\u0026gt;\n        \u0026lt;td\u0026gt;\u0026lt;%= book.publisher %\u0026gt;\u0026lt;/td\u0026gt;\n        \u0026lt;td\u0026gt;\u0026lt;%= book.category %\u0026gt;\u0026lt;/td\u0026gt;\n        \u0026lt;td\u0026gt;\u0026lt;%= link_to \"Delete\", book_path(book, page: page.number), data: { 'turbo-method': :delete, 'turbo-confirm': \"Are you sure?\" } %\u0026gt;\u0026lt;/td\u0026gt;\n      \u0026lt;/tr\u0026gt;\n    \u0026lt;% end %\u0026gt;\n  \u0026lt;/tbody\u0026gt;\n\u0026lt;/table\u0026gt; \u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe first part of this partial is the pagination code that moves the page back and forth. The recordset count will show how many records are left, so we can confirm that a Book was deleted successfully. The \u003ccode\u003eDelete\u003c/code\u003e link will include the current page in the request so the paginate method will load the correct set of records. It also uses the \u003ccode\u003eturbo-method\u003c/code\u003e and the \u003ccode\u003eturbo-confirm\u003c/code\u003e data attributes, which are different from the traditional Rails UJS data annotations. They behave the same, but will be used by Turbo instead of Rails UJS.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003eindex.html.erb\u003c/code\u003e will have a \u003ccode\u003eturbo_frame_tag\u003c/code\u003e so that Turbo knows which section to replace. The \u003ccode\u003eaction\u003c/code\u003e is supposed to update the page’s URL so that when the page reloads, it shows the proper page of records.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;h1 class=\"title\"\u0026gt;Paginate Through Books\u0026lt;/h1\u0026gt;\n\u0026lt;%= turbo_frame_tag \"books\", action: \"advance\" do %\u0026gt;\n  \u0026lt;%= render partial: 'book_records', locals: {page: @page} %\u0026gt;\n\u0026lt;% end %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe \u003ccode\u003edestroy.turbo_stream.erb\u003c/code\u003e will use the replace command to reload the page of records. There is no need to remove the destroyed Book, because it is no longer in the database and won’t appear.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;%= turbo_stream.replace \"books\" do %\u0026gt;\n  \u0026lt;%= turbo_frame_tag \"books\", action: \"advance\" do %\u0026gt;\n    \u0026lt;%= render partial: 'book_records', locals: {page: @page} %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNotice how both actions use the \u003ccode\u003ebook_records\u003c/code\u003e partial and pass the \u003ccode\u003e@page\u003c/code\u003e variable as a local parameter.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAmazing Interactivity\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOne of the great things about HOTWire is how very little changes in the controllers and views. By rethinking how the destroy method is used, we can get a very interactive app without having to build any front end JavaScript.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2021/03/02/rails-pwas-using-turbo-hhnpwa-7","url":"https://onrails.blog/2021/03/02/rails-pwas-using-turbo-hhnpwa-7","title":"Rails PWAs using Turbo HHNPWA #7","date_published":"2021-03-02T20:53:02Z","date_modified":"2026-06-02T00:41:05Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eDoes your app even need to be a Progressive Web App? The answer lies in what is missing from your web app.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWould it help to have some information available in the situation where no internet connection is available?\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eDo you need to be available immediately, just like a native app?\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eProgressive Web Apps, even if visited through the normal browser chrome, can provide some nice enhancements to your web app’s experience. Thankfully, the minimum configuration to get this experience is very small. If you’ve been following along, you’ll see we started with a regular, performant and interactive Rails app, and only now are we going to turn on the PWA switch.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eSetting it up\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhat makes any web site a progressive web app? Two small pieces tell the browser to treat your web page as a progressive web app. You’ll need a:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:list --\u003e\n\u003cul\u003e\u003c!-- wp:list-item --\u003e\n\u003cli\u003eJSON Manifest\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\n\n\u003c!-- wp:list-item --\u003e\n\u003cli\u003eService worker JavaScript file\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\u003c/ul\u003e\n\u003c!-- /wp:list --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe browser now grants your app the ability to run a parallel JavaScript process with the ability to inspect and modify all HTTP requests. The biggest advantage of having a service worker is offline access. Offline access stores important information on device, or give your user a small experience of the full online experience. I think the biggest gap in PWAs is that most services require a constant online connection. However, perhaps a Calendar or ToDo service could perform well in this situation. They could keep a cached copy of your events or todos, and then update any changes when the network is available.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eAdding the Required Files\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThere are a number of techniques for making your Rails App a PWA. The biggest challenge with the current asset pipeline / webpacker setup is sending the service worker to the client. The PWA specification sets the root of the service worker at the path that it’s served from. If it’s \u003ccode\u003eexample.org/service-worker.js\u003c/code\u003e, then the service worker gets to proxy all traffic for \u003ccode\u003eexample.org\u003c/code\u003e. However, if it’s \u003ccode\u003eexample.org/packs/js/service-worker.js\u003c/code\u003e, then it can \u003cem\u003eonly\u003c/em\u003e proxy requests under \u003ccode\u003e/packs/js/\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003ePutting the service worker and manifest file as JSON templates lets you use the most of the Rails stack. You can specify images through the asset pipeline, and you can use routing to prefetch pages that should be available online.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThis can be done with a controller called \u003ccode\u003eservice_worker_controller.rb\u003c/code\u003e and two methods, like so:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eclass ServiceWorkerController \u0026lt; ApplicationController\n  protect_from_forgery except: :service_worker\n\n  def service_worker\n  end\n\n  def manifest\n  end\nend\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThe protect from forgery for the \u003ccode\u003eservice_worker\u003c/code\u003e method allows the javascript to be served without any forgery request problems in rails.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003cbr\u003eUpdate the routes file:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eget '/service-worker.js' =\u0026gt; \"service_worker#service_worker\"\nget '/manifest.json' =\u0026gt; \"service_worker#manifest\"\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the views folder, make a folder called \u003ccode\u003eservice_worker\u003c/code\u003e and add a \u003ccode\u003emanifest.json.erb\u003c/code\u003e file that looks something like this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e{\n\"short_name\": \"HHNWPA\",\n\"name\": \"HOTWire Hacker News Progressive Web App\",\n\"icons\": [\n  {\n    \"src\": \"\u0026lt;%= asset_path('icon_192.png') %\u0026gt;\",\n    \"type\": \"image/png\",\n    \"sizes\": \"192x192\"\n  },\n  {\n    \"src\": \"\u0026lt;%= asset_path('icon_512.png') %\u0026gt;\",\n    \"type\": \"image/png\",\n    \"sizes\": \"512x512\"\n  }\n],\n\"start_url\": \"\u0026lt;%= root_path %\u0026gt;\",\n\"background_color\": \"#fff\",\n\"display\": \"standalone\",\n\"scope\": \"\u0026lt;%= root_path %\u0026gt;\",\n\"theme_color\": \"#000\"\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAsset paths are available, with means that all the Rails asset pipeline or WebPacker features are available. Those icons referenced above also need to be provided, so if you don’t have them, remove them from the manifest.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eNow, add a \u003ccode\u003eservice-worker.js.erb\u003c/code\u003e file that looks something like this:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003evar CACHE_VERSION = 'v1';\nvar CACHE_NAME = CACHE_VERSION + ':sw-cache-';\n\nfunction onInstall(event) {\n  console.log('[Serviceworker]', \"Installing!\", event);\n  event.waitUntil(\n    caches.open(CACHE_NAME).then(function prefill(cache) {\n      return cache.addAll([\n        '\u0026lt;%= asset_pack_path 'application.js' %\u0026gt;',\n        '\u0026lt;%= asset_pack_path 'application.css' %\u0026gt;',\n        '\u0026lt;%= asset_path 'application.css' %\u0026gt;'\n      ]);\n    })\n  );\n}\n\nfunction onActivate(event) {\n  console.log('[Serviceworker]', \"Activating!\", event);\n  event.waitUntil(\n    caches.keys().then(function(cacheNames) {\n      return Promise.all(\n        cacheNames.filter(function(cacheName) {\n          // Return true if you want to remove this cache,\n          // but remember that caches are shared across\n          // the whole origin\n          return cacheName.indexOf(CACHE_VERSION) !== 0;\n        }).map(function(cacheName) {\n          return caches.delete(cacheName);\n        })\n      );\n    })\n  );\n}\n\nfunction onFetch(event) {\n  event.respondWith(\n    // try to return untouched request from network first\n    fetch(event.request).catch(function() {\n      // if it fails, try to return request from the cache\n      return caches.match(event.request).then(function(response) {\n        if (response) {\n          return response;\n        }\n        // if not found in cache, return default offline content for navigate requests\n        if (event.request.mode === 'navigate' ||\n          (event.request.method === 'GET' \u0026amp;\u0026amp; event.request.headers.get('accept').includes('text/html'))) {\n          console.log('[Serviceworker]', \"Fetching offline content\", event);\n          return caches.match('/offline.html');\n        }\n      })\n    })\n  );\n}\n\nself.addEventListener('install', onInstall);\nself.addEventListener('activate', onActivate);\nself.addEventListener('fetch', onFetch);\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eWhatever files need caching are added in the \u003ccode\u003eonInstall\u003c/code\u003e function. This template only has the webpack pack files and the asset pipeline CSS, but any images, or asset pipeline files that should be cached can be added here.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eLoading the service worker with Stimulus\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI’ve found a Stimulus controller can load the Service Worker, and it neatly keeps JavaScript out of your other ERB templates. It also provides a cleaner JavaScript communication point between your HTML and the Service Worker.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"xml\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;div data-controller=\"service-worker\"\u0026gt;\n  \u0026lt;!-- HTML Code --\u0026gt;\n\u0026lt;/div\u0026gt; \u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIt’s weird to say it, since this is the seventh tutorial on this website, but we’ll need to install Stimulus.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"bash\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003erails webpacker:install:stimulus\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIn the \u003ccode\u003econnect()\u003c/code\u003e function, the controller sees if the Service worker is running.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf the service worker isn’t running, the controller registers the service worker, and sets up a chain of callbacks, waiting for the installation and activation to complete.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHere is \u003ccode\u003eservice_worker_controller.js\u003c/code\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"jscript\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003eimport { Controller } from \"stimulus\";\nexport default class extends Controller {\n  connect() {\n    if (navigator.serviceWorker) {\n      if (navigator.serviceWorker.controller) {\n        // If the service worker is already running, skip to state change\n        this.stateChange();\n      } else {\n        // Register the service worker, and wait for it to become active\n        navigator.serviceWorker\n          .register(\"/service-worker.js\", { scope: \"./\" })\n          .then(function (reg) {\n            console.log(\"[Companion]\", \"Service worker registered!\");\n            console.log(reg);\n          });\n        navigator.serviceWorker.addEventListener(\n          \"controllerchange\",\n          this.controllerChange.bind(this)\n        );\n      }\n    }\n  }\n\n  controllerChange(event) {\n    navigator.serviceWorker.controller.addEventListener(\n      \"statechange\",\n      this.stateChange.bind(this)\n    );\n  }\n\n  stateChange() {\n    // perform any visual manipulations here\n  }\n}\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eTesting?\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eGoogle’s \u003ca href=\"https://github.com/GoogleChrome/lighthouse\"\u003eLighthouse\u003c/a\u003e is the best way to test how an app matches the specification. Lighthouse will give recommendations, and list what’s missing. It can even test accessibility. Progressive Web Apps need HTTPS when not being served from localhost, so testing in Google Chrome during development is the easiest option.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eNext Steps\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eSubscribe for updates as this project takes shape. You can see all the code at Github: \u003ca href=\"https://github.com/OnRailsBlog/hhnpwa\"\u003ehttps://github.com/OnRailsBlog/hhnpwa\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eOther HOTWire Tutorials\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:list --\u003e\n\u003cul\u003e\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://onrails.blog/2020/12/23/building-hhnpwa-1-setting-up-for-top-stories/\"\u003eHOTWire HNPWA Tutorial #1: Setting up for Top Stories\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\n\n\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://onrails.blog/2020/12/27/building-hhnpwa-2-streaming-top-items/\"\u003eHOTWire HNPWA Tutorial #2: Turbo Streaming Top Items\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\n\n\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://onrails.blog/2020/12/28/building-hhnpwa-3-top-item-pagination/\"\u003eHOTWire HNPWA Tutorial #3: Paginating Top Items\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\n\n\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://onrails.blog/2020/12/29/russian-doll-caching-building-hotwire-hnpwa-4/\"\u003eHOTWire HNPWA Tutorial #4:Russian Doll Caching\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\n\n\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://onrails.blog/2021/01/07/lazy-loading-lots-of-comments-hotwire-tutorial-5/\"\u003eHOTWire HNPWA Tutorial #5: Lazy Loading Lots of Comments\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\n\n\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://onrails.blog/2021/01/22/improving-performance-with-russian-doll-caching-hotwire-tutorial-6/\"\u003eHOTWire HNPWA Tutorial #6: Improving Performance with Russian Doll Caching\u003c/a\u003e\u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\n\n\u003c!-- wp:list-item --\u003e\n\u003cli\u003e\u003ca href=\"https://onrails.blog/2021/03/02/rails-pwas-using-turbo-hhnpwa-7/\"\u003eHOTWire HNPWA Tutorial #7: Adding PWA Service Worker\u003c/a\u003e \u003c/li\u003e\n\u003c!-- /wp:list-item --\u003e\u003c/ul\u003e\n\u003c!-- /wp:list --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2021/02/18/hotwiring-an-existing-rails-monolith-forms","url":"https://onrails.blog/2021/02/18/hotwiring-an-existing-rails-monolith-forms","title":"HOTWiring an existing Rails Monolith: Forms!","date_published":"2021-02-18T14:20:23Z","date_modified":"2026-06-02T00:41:05Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eLet’s say your majestic monolith is looking for more interactivity, and you’ve picked up \u003ca href=\"https://turbo.hotwire.dev\"\u003eTurbo\u003c/a\u003e. What aspects of a Rails app change that may cause some headaches? \u003ca href=\"https://turbo.hotwire.dev/handbook/installing\"\u003eAdd Turbo\u003c/a\u003e, and then follow along!\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:heading --\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eForms\u003c/h2\u003e\n\u003c!-- /wp:heading --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eFirst, you need to test all your forms. Turbo dramatically changes the behavior of each form. The biggest change you will see is that your form redirects don’t behave the same way. Since Turbo is adding Single Page Application (SPA) functionality to your app, you will likely need to reengineer the frontend to behave more like a SPA. I think the quickest way to get Turbo working, and give you flexibility to incrementally update your forms is turn off remote forms, and tell Turbo to ignore the form. This worked best on my forms that made some change, and then redirected to a new page. This meant adding some \u003ccode\u003edata-turbo\u003c/code\u003e and \u003ccode\u003edata-remote\u003c/code\u003e attributes, like so:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003e\u0026lt;%= form_with model: @order, url: orders_path, data: { turbo: false, remote: false } do |form| %\u0026gt;\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eSetting the remote and turbo data attributes to false makes the form submission occur outside the JavaScript environment. This will blow away the JavaScript environment, so you’ll want to figure out a way reengineer your forms, which I’ll show you in the next section.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI don’t want to indict Turbo at all for this change. Adding Turbo is a rethinking of the way your Rails app interacts with the browser, and this is a stop gap to your existing app works, and you can refactor the front end to include more interactivity.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]},{"id":"https://onrails.blog/2021/01/28/hotwiring-an-existing-rails-monolith","url":"https://onrails.blog/2021/01/28/hotwiring-an-existing-rails-monolith","title":"HOTWiring an Existing Rails Monolith","date_published":"2021-01-28T09:14:21Z","date_modified":"2026-06-02T00:41:04Z","content_html":"\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eHow do you add \u003ca href=\"https://turbo.hotwire.dev\"\u003eTurbo\u003c/a\u003e to your existing Rails app? What do you need to watch out for as you transition to a full HOTWire approach? I found it to be very straight forward, and mostly a search and replace operation.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eAdd \u003ca href=\"https://github.com/hotwired/turbo-rails\"\u003eTurbo Rails gem\u003c/a\u003e to the Gemfile and remove Turbolinks:\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:syntaxhighlighter/code {\"language\":\"ruby\",\"className\":\"not-prose\"} --\u003e\n\u003cpre class=\"wp-block-syntaxhighlighter-code not-prose\"\u003egem 'turbo-rails'\u003c/pre\u003e\n\u003c!-- /wp:syntaxhighlighter/code --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eThen run \u003ccode\u003e./bin/bundle install\u003c/code\u003e and\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003erun \u003ccode\u003e./bin/rails turbo:install\u003c/code\u003e\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eSearch through your code base for \u003ccode\u003eturbolinks\u003c/code\u003e to see where you are using it. The first change I found was in the html layouts. \u003ccode\u003e'data-turbolinks-track'\u003c/code\u003e became \u003ccode\u003e'data-turbo-track'\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eYou may have configured some links that you didn’t want Turbolinks to follow, by setting the \u003ccode\u003edata-turbolinks\u003c/code\u003e attribute to false. This attribute is now \u003ccode\u003edata-turbo\u003c/code\u003e, so look for those throughout your code base.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eYou may have used the permanent attribute, \u003ccode\u003edata-turbolinks-permanent\u003c/code\u003e, which kept elements between page visits, and mimicked the behavior of single page applications. This attribute is now \u003ccode\u003edata-turbo-permanent\u003c/code\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eIf you have any JavaScript code that listened for \u003ccode\u003eturbolinks\u003c/code\u003e events, you should make sure the names of the events now start with \u003ccode\u003eturbo\u003c/code\u003e instead of \u003ccode\u003eturbolinks\u003c/code\u003e. I have a couple Stimulus controllers that listen to the \u003ccode\u003eturbolinks:before-visit\u003c/code\u003e event, so those are now \u003ccode\u003eturbo:before-visit\u003c/code\u003e. You can see all the \u003ca href=\"https://turbo.hotwire.dev/reference/events\"\u003eTurbo events\u003c/a\u003e and compare them with the \u003ca href=\"https://github.com/turbolinks/turbolinks#full-list-of-events\"\u003eTurbolinks events\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eOf course, run your tests after all these changes, and QA all your pages to make sure nothing is broken.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:paragraph --\u003e\n\u003cp\u003eI’m looking forward to taking advantage of more of the features of Turbo, including lazy loading frames and an easier to use broadcast method for changes on the page.\u003c/p\u003e\n\u003c!-- /wp:paragraph --\u003e\n\n\u003c!-- wp:mailpoet/subscription-form-block {\"formId\":1} /--\u003e","tags":["Posts"]}]}