{"generator":"Bridgetown","link":[{"@attributes":{"href":"https:\/\/michaelwapp.com\/feed.xml","rel":"self","type":"application\/atom+xml"}},{"@attributes":{"href":"https:\/\/michaelwapp.com\/","rel":"alternate","type":"text\/html"}}],"updated":"2026-02-15T12:24:12+00:00","id":"https:\/\/michaelwapp.com\/feed.xml","title":"Michael Bauer-Wapp","subtitle":"Personal website of Michael Bauer-Wapp, an independent software developer based in Austria.","entry":[{"title":"How To: Ruby on Rails, git worktrees and Claude Code","link":{"@attributes":{"href":"https:\/\/michaelwapp.com\/ruby-on-rails\/git-worktrees\/claude-code\/ai\/2026\/02\/15\/how-to-ruby-on-rails-git-worktrees-and-claude-code\/","rel":"alternate","type":"text\/html","title":"How To: Ruby on Rails, git worktrees and Claude Code"}},"published":"2026-02-15T12:00:00+00:00","updated":"2026-02-15T12:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2026-02-15-how-to-ruby-on-rails-git-worktrees-and-claude-code.md","content":"<h3 id=\"introduction\">Introduction<\/h3>\n\n<p>If there is one thing which makes coding with AI go from being a helper to being an entire development process, it\u2019s the use of independent workable copies of your code. And what I mean by that is: being able to work on multiple features or ideas at once, independently, without breaking anything \u2014 because your environments are so isolated that it\u2019s a breeze to use AI for your agentic development process.<\/p>\n\n<p>This is a How To guide where I show you how I use git worktrees and isolated Ruby on Rails environments to enable independent coding using <a href=\"https:\/\/docs.anthropic.com\/en\/docs\/claude-code\/overview\">Claude Code<\/a>.<\/p>\n\n<h3 id=\"the-required-tooling\">The required tooling<\/h3>\n\n<p>Before we start, let me explain what tools are required to set this up:<\/p>\n\n<ul>\n  <li><strong><a href=\"https:\/\/git-scm.com\/docs\/git-worktree\">Git worktrees<\/a><\/strong> \u2014 linked working directories from the same repo, each on a different branch. They\u2019re not clones \u2014 they share the same <code class=\"highlighter-rouge\">.git<\/code> history. If you already have git installed, you already have worktrees. No extra install needed.<\/li>\n  <li><strong>Ruby on Rails<\/strong> \u2014 with a standard development setup (Postgres, Redis, Sidekiq).<\/li>\n  <li><strong>Postgres &amp; Redis<\/strong> \u2014 either installed locally or running via Docker. Both work fine, Docker just makes it easier to keep things tidy.<\/li>\n  <li><strong><a href=\"https:\/\/docs.anthropic.com\/en\/docs\/claude-code\/overview\">Claude Code<\/a><\/strong> \u2014 Anthropic\u2019s CLI agent for coding.<\/li>\n<\/ul>\n\n<p>I assume you\u2019re familiar with Rails development and how to install Claude Code. The new piece here is git worktrees. Think of them like this: instead of cloning the repo twice, you create a second working directory that points at the same repository. Both directories see the same branches, commits and history \u2014 but each one has its own checked-out branch and its own working files. That\u2019s the foundation for everything below.<\/p>\n\n<h3 id=\"the-changes\">The changes<\/h3>\n\n<p>The first change is your mindset. Don\u2019t think of this as simply \u201cfeature branches\u201d \u2014 think of each worktree as an independent developer working on your project. You\u2019re the orchestrator.<\/p>\n\n<p>The most basic way of using worktrees is <code class=\"highlighter-rouge\">git worktree add ..\/my-feature feature-branch<\/code>. But your codebase isn\u2019t ready for that kind of independence yet. Each worktree needs its own database, its own port, and its own Redis namespace. Let\u2019s fix that.<\/p>\n\n<h4 id=\"a-env-driven-databaseyml\">a) ENV-driven database.yml<\/h4>\n\n<p>Strip out all hardcoded database names and make everything environment-driven:<\/p>\n\n<div class=\"language-yaml highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"na\">default<\/span><span class=\"pi\">:<\/span> <span class=\"nl\">&amp;default<\/span>\n  <span class=\"na\">adapter<\/span><span class=\"pi\">:<\/span> <span class=\"s\">postgresql<\/span>\n  <span class=\"na\">encoding<\/span><span class=\"pi\">:<\/span> <span class=\"s\">unicode<\/span>\n  <span class=\"na\">database<\/span><span class=\"pi\">:<\/span> \n  <span class=\"na\">username<\/span><span class=\"pi\">:<\/span> \n  <span class=\"na\">password<\/span><span class=\"pi\">:<\/span> \n  <span class=\"na\">host<\/span><span class=\"pi\">:<\/span> \n  <span class=\"na\">port<\/span><span class=\"pi\">:<\/span> \n  <span class=\"na\">pool<\/span><span class=\"pi\">:<\/span> <span class=\"m\">5<\/span>\n\n<span class=\"na\">development<\/span><span class=\"pi\">:<\/span>\n  <span class=\"na\">&lt;&lt;<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">*default<\/span>\n\n<span class=\"na\">test<\/span><span class=\"pi\">:<\/span>\n  <span class=\"na\">&lt;&lt;<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">*default<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Now each worktree can point at its own database just by setting <code class=\"highlighter-rouge\">DATABASE_NAME<\/code>.<\/p>\n\n<h4 id=\"b-shared-postgres--redis\">b) Shared Postgres &amp; Redis<\/h4>\n\n<p>All worktrees share the same Postgres and Redis instances \u2014 they just use different database names and Redis DB numbers. If you\u2019re running them locally, you\u2019re already set. If you prefer Docker, here\u2019s a simple <code class=\"highlighter-rouge\">docker-compose.yml<\/code>:<\/p>\n\n<div class=\"language-yaml highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"na\">services<\/span><span class=\"pi\">:<\/span>\n  <span class=\"na\">db<\/span><span class=\"pi\">:<\/span>\n    <span class=\"na\">image<\/span><span class=\"pi\">:<\/span> <span class=\"s\">postgres:17-alpine<\/span>\n    <span class=\"na\">volumes<\/span><span class=\"pi\">:<\/span>\n      <span class=\"pi\">-<\/span> <span class=\"s\">db_data:\/var\/lib\/postgresql\/data<\/span>\n    <span class=\"na\">environment<\/span><span class=\"pi\">:<\/span>\n      <span class=\"na\">POSTGRES_USER<\/span><span class=\"pi\">:<\/span> <span class=\"s\">postgres<\/span>\n      <span class=\"na\">POSTGRES_PASSWORD<\/span><span class=\"pi\">:<\/span> <span class=\"s\">password<\/span>\n    <span class=\"na\">ports<\/span><span class=\"pi\">:<\/span>\n      <span class=\"pi\">-<\/span> <span class=\"s2\">\"<\/span><span class=\"s\">${DOCKER_POSTGRES_PORT:-5431}:5432\"<\/span>\n\n  <span class=\"na\">redis<\/span><span class=\"pi\">:<\/span>\n    <span class=\"na\">image<\/span><span class=\"pi\">:<\/span> <span class=\"s\">redis:7-alpine<\/span>\n    <span class=\"na\">command<\/span><span class=\"pi\">:<\/span> <span class=\"s\">redis-server<\/span>\n    <span class=\"na\">ports<\/span><span class=\"pi\">:<\/span>\n      <span class=\"pi\">-<\/span> <span class=\"s2\">\"<\/span><span class=\"s\">${DOCKER_REDIS_PORT:-6380}:6379\"<\/span>\n\n<span class=\"na\">volumes<\/span><span class=\"pi\">:<\/span>\n  <span class=\"na\">db_data<\/span><span class=\"pi\">:<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The non-default ports (5431\/6380) avoid collisions with any locally installed Postgres or Redis. Adjust to match your setup \u2014 the important thing is that every worktree talks to the same services but uses a different database name and Redis DB number.<\/p>\n\n<h4 id=\"c-auto-derive-environment-per-worktree\">c) Auto-derive environment per worktree<\/h4>\n\n<p>This is the core trick. A <code class=\"highlighter-rouge\">bin\/worktree-env<\/code> script reads the directory name and derives all the environment variables from it. Let\u2019s say your app is called <code class=\"highlighter-rouge\">myapp<\/code>:<\/p>\n\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c\">#!\/usr\/bin\/env bash<\/span>\n<span class=\"nv\">WORKTREE_NAME<\/span><span class=\"o\">=<\/span><span class=\"si\">$(<\/span><span class=\"nb\">basename<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$PWD<\/span><span class=\"s2\">\"<\/span><span class=\"si\">)<\/span>\n<span class=\"nv\">WORKTREE_SUFFIX<\/span><span class=\"o\">=<\/span><span class=\"si\">$(<\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$WORKTREE_NAME<\/span><span class=\"s2\">\"<\/span> | <span class=\"nb\">sed<\/span> <span class=\"s1\">'s\/^myapp-*\/\/'<\/span> | <span class=\"nb\">sed<\/span> <span class=\"s1\">'s\/-\/_\/g'<\/span><span class=\"si\">)<\/span>\n\n<span class=\"k\">if<\/span> <span class=\"o\">[<\/span> <span class=\"nt\">-z<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$WORKTREE_SUFFIX<\/span><span class=\"s2\">\"<\/span> <span class=\"o\">]<\/span> <span class=\"o\">||<\/span> <span class=\"o\">[<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$WORKTREE_SUFFIX<\/span><span class=\"s2\">\"<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">\"myapp\"<\/span> <span class=\"o\">]<\/span><span class=\"p\">;<\/span> <span class=\"k\">then<\/span>\n  <span class=\"c\"># Main worktree<\/span>\n  <span class=\"nv\">DATABASE_NAME<\/span><span class=\"o\">=<\/span><span class=\"s2\">\"<\/span><span class=\"k\">${<\/span><span class=\"nv\">DATABASE_NAME<\/span><span class=\"k\">:-<\/span><span class=\"nv\">myapp_development<\/span><span class=\"k\">}<\/span><span class=\"s2\">\"<\/span>\n  <span class=\"nv\">PORT<\/span><span class=\"o\">=<\/span><span class=\"s2\">\"<\/span><span class=\"k\">${<\/span><span class=\"nv\">PORT<\/span><span class=\"k\">:-<\/span><span class=\"nv\">3000<\/span><span class=\"k\">}<\/span><span class=\"s2\">\"<\/span>\n  <span class=\"nv\">REDIS_DB<\/span><span class=\"o\">=<\/span><span class=\"s2\">\"<\/span><span class=\"k\">${<\/span><span class=\"nv\">REDIS_DB<\/span><span class=\"k\">:-<\/span><span class=\"nv\">1<\/span><span class=\"k\">}<\/span><span class=\"s2\">\"<\/span>\n<span class=\"k\">else<\/span>\n  <span class=\"c\"># Feature worktree \u2014 derive everything from the directory name<\/span>\n  <span class=\"nv\">DATABASE_NAME<\/span><span class=\"o\">=<\/span><span class=\"s2\">\"<\/span><span class=\"k\">${<\/span><span class=\"nv\">DATABASE_NAME<\/span><span class=\"k\">:-<\/span><span class=\"nv\">myapp_<\/span><span class=\"k\">${<\/span><span class=\"nv\">WORKTREE_SUFFIX<\/span><span class=\"k\">}<\/span><span class=\"nv\">_development<\/span><span class=\"k\">}<\/span><span class=\"s2\">\"<\/span>\n  <span class=\"nv\">PORT<\/span><span class=\"o\">=<\/span><span class=\"k\">$((<\/span> <span class=\"m\">3001<\/span> <span class=\"o\">+<\/span> <span class=\"si\">$(<\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$WORKTREE_SUFFIX<\/span><span class=\"s2\">\"<\/span> | <span class=\"nb\">cksum<\/span> | <span class=\"nb\">cut<\/span> <span class=\"nt\">-d<\/span><span class=\"s1\">' '<\/span> <span class=\"nt\">-f1<\/span><span class=\"si\">)<\/span> <span class=\"o\">%<\/span> <span class=\"m\">99<\/span> <span class=\"k\">))<\/span>\n  <span class=\"nv\">REDIS_DB<\/span><span class=\"o\">=<\/span><span class=\"k\">$((<\/span> <span class=\"si\">$(<\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"<\/span><span class=\"nv\">$WORKTREE_SUFFIX<\/span><span class=\"s2\">\"<\/span> | <span class=\"nb\">cksum<\/span> | <span class=\"nb\">cut<\/span> <span class=\"nt\">-d<\/span><span class=\"s1\">' '<\/span> <span class=\"nt\">-f1<\/span><span class=\"si\">)<\/span> <span class=\"o\">%<\/span> <span class=\"m\">15<\/span> <span class=\"o\">+<\/span> <span class=\"m\">1<\/span> <span class=\"k\">))<\/span>\n<span class=\"k\">fi\n\n<\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"export DATABASE_NAME='<\/span><span class=\"nv\">$DATABASE_NAME<\/span><span class=\"s2\">'\"<\/span>\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\"export PORT='<\/span><span class=\"nv\">$PORT<\/span><span class=\"s2\">'\"<\/span>\n<span class=\"nb\">echo<\/span> <span class=\"s2\">\"export REDIS_URL='redis:\/\/localhost:<\/span><span class=\"k\">${<\/span><span class=\"nv\">REDIS_PORT<\/span><span class=\"k\">:-<\/span><span class=\"nv\">6379<\/span><span class=\"k\">}<\/span><span class=\"s2\">\/<\/span><span class=\"nv\">$REDIS_DB<\/span><span class=\"s2\">'\"<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Directory <code class=\"highlighter-rouge\">myapp-feature-auth<\/code> \u2192 database <code class=\"highlighter-rouge\">myapp_feature_auth_development<\/code>, port <code class=\"highlighter-rouge\">3042<\/code> (or whatever the hash lands on), Redis DB <code class=\"highlighter-rouge\">7<\/code>. Fully deterministic, no manual config.<\/p>\n\n<h4 id=\"d-procfiledev\">d) Procfile.dev<\/h4>\n\n<p>The Procfile just references <code class=\"highlighter-rouge\">${PORT}<\/code> so each worktree starts on its own port:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>web: bin\/rails server -p ${PORT:-3000}\ncss: bin\/rails dartsass:watch\nsidekiq: bundle exec sidekiq -C config\/sidekiq.yml\n<\/code><\/pre><\/div><\/div>\n\n<h3 id=\"the-flow-state\">The flow state<\/h3>\n\n<p>Once you understand the concept, it\u2019s time to automate. I added three scripts to <code class=\"highlighter-rouge\">bin\/<\/code> that handle the full lifecycle:<\/p>\n\n<p><strong><code class=\"highlighter-rouge\">bin\/wt-new<\/code><\/strong> \u2014 create a new worktree<\/p>\n\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>bin\/wt-new feature-auth              <span class=\"c\"># branch off current branch<\/span>\nbin\/wt-new feature-auth develop      <span class=\"c\"># branch off develop<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>What it does:<\/p>\n<ul>\n  <li>Creates <code class=\"highlighter-rouge\">..\/myapp-feature-auth\/<\/code> as a git worktree on a new branch<\/li>\n  <li>Generates a <code class=\"highlighter-rouge\">.env.local<\/code> with the derived <code class=\"highlighter-rouge\">DATABASE_NAME<\/code>, <code class=\"highlighter-rouge\">PORT<\/code> and <code class=\"highlighter-rouge\">REDIS_URL<\/code><\/li>\n  <li>Runs the full setup: <code class=\"highlighter-rouge\">bundle install<\/code>, <code class=\"highlighter-rouge\">yarn install<\/code>, <code class=\"highlighter-rouge\">db:prepare<\/code>, <code class=\"highlighter-rouge\">db:seed<\/code><\/li>\n<\/ul>\n\n<p>You end up with a ready-to-run Rails environment in seconds.<\/p>\n\n<p><strong><code class=\"highlighter-rouge\">bin\/wt-list<\/code><\/strong> \u2014 see what\u2019s running<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n                          Git Worktrees\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n  \u25cf myapp                          :3000  develop\n    myapp-feat-new-folder-design   :3002  feat\/new-folder-design\n\n  \u25cf = Running\n<\/code><\/pre><\/div><\/div>\n\n<p>Shows every worktree, its port, its branch, and whether it\u2019s currently running.<\/p>\n\n<p><strong><code class=\"highlighter-rouge\">bin\/wt-rm<\/code><\/strong> - clean up when you\u2019re done<\/p>\n\n<ul>\n  <li>Drops the worktree\u2019s Postgres database<\/li>\n  <li>Removes the worktree directory<\/li>\n  <li>Optionally deletes the branch (with a confirmation prompt)<\/li>\n<\/ul>\n\n<p>Now here\u2019s where it gets fun. Open a separate terminal in each worktree directory and run <code class=\"highlighter-rouge\">claude<\/code> in each one. Each Claude Code session sees only that worktree\u2019s files and branch. You can have one session implementing a feature, another fixing a bug, and a third refactoring tests - all at the same time, all completely isolated. You review and merge when each one is done.<\/p>\n\n<p>That\u2019s the flow state: you stop being the person who writes every line and start being the person who directs the work and reviews the output.<\/p>\n\n<h3 id=\"conclusion\">Conclusion<\/h3>\n\n<p>And that\u2019s it. That\u2019s the magic. It\u2019s amazing what you can achieve with this setup. You are much faster and can work on multiple features at once. Or review code while a big feature is churning away in another worktree. It\u2019s proper development orchestration.<\/p>\n\n<p>Just make sure to have proper safeguards in place to not produce code which doesn\u2019t do what you want it to do. But I will talk about that in another article in the future.<\/p>\n\n<p>Cheers,\nMichael<\/p>","author":{"name":{}}},{"title":"The most efficient way to use your MacBook","link":{"@attributes":{"href":"https:\/\/michaelwapp.com\/tiling-window-manager\/linux\/macos\/2026\/02\/14\/a-modern-tiling-manager-for-macos-26\/","rel":"alternate","type":"text\/html","title":"The most efficient way to use your MacBook"}},"published":"2026-02-14T12:00:00+00:00","updated":"2026-02-14T12:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2026-02-14-a-modern-tiling-manager-for-macos-26.md","content":"<h2 id=\"introduction\">Introduction<\/h2>\n\n<p>My computer usage history is quite diverse. As any 90s kid, I started off \u2014 of course \u2014 with Windows. The first experience using a computer was at my cousin\u2019s place using his Windows 98 and then Windows 2000 setup. I felt right at home \u2014 it was exciting to use a computer and I was hooked. Once I convinced my parents to get my own PC, Windows XP was already out and therefore I consider Windows XP as my first true computing experience.<\/p>\n\n<p>Initially without Internet, Minesweeper, Ping Pong and store bought games and Paint filled all my computing needs. I was spending quite some time playing games. I enjoyed Stronghold, Age of Empires and Tony Hawk.<\/p>\n\n<p>Fast forward to Windows 7 \u2014 a more polished operating system arrived \u2014 and I loved it. It felt modern to me. I grew up with it and it was my main operating system until I started with university where the need for a laptop arose. I started my computer science studies with a switch to macOS and bought myself a new MacBook Air 13.3 running macOS Mountain Lion. I loved the experience.<\/p>\n\n<h2 id=\"the-big-change\">The Big Change<\/h2>\n\n<p>I noticed something during the beginning of university: There were three types of people. The Windows kids, the Apple fanboys and the Linux nerds. And as I studied computer science, the split was more like 10% Windows, 50% Linux and 40% macOS. And my curiosity kicked in. What is Linux like? Why is everyone using it? And why on earth is everyone running Linux on these office laptops called ThinkPads. But I got curious and wanted to see what the experience was. And so I bought myself an X240 ThinkPad and installed Arch Linux on it. Yes, you read that right. I didn\u2019t start with Ubuntu, I went full nerd and installed Arch.<\/p>\n\n<p>Additionally to Arch, I found the <a href=\"https:\/\/www.reddit.com\/r\/unixporn\/\">unixporn<\/a> subreddit and the concept of a tiling window manager was introduced to me. Therefore, I went and installed <a href=\"https:\/\/i3wm.org\/\">i3wm<\/a> and I loved it. Once I played around I started to use this system more and more in daily tasks and kind of switched over from my MacBook. Eventually, I bought myself an X1 Carbon 14inch and used Linux as my main operating system for 1,5 years.<\/p>\n\n<h2 id=\"the-switchback\">The Switchback<\/h2>\n\n<p>Due to work reasons and the interest in mobile app engineering, I switched back to a MacBook in early 2019. My love for Linux is still there and I kept my X240 as a \u201ctinkering device\u201d. I\u2019ve used 15inch MacBook Pros and then a 14inch MacBook Pro and recently a MacBook Pro 16 inch with an M2 Processor. The main reason for the 16inch device is that I\u2019m constantly on the road and I need the screen real estate to work efficiently. 14inch was too small and felt slightly cramped to work with. But I was missing the ease of use of a tiling window manager, especially when you are working only on a laptop without an external monitor. And then I stumbled upon something magical for macOS.<\/p>\n\n<h2 id=\"aerospace--the-missing-tiling-window-manager-for-macos\">AeroSpace \u2014 The Missing Tiling Window Manager for macOS<\/h2>\n\n<p><a href=\"https:\/\/github.com\/nikitabobko\/AeroSpace\">AeroSpace<\/a> is a tiling window manager for macOS inspired by i3. It came into my life in the middle of 2025 and it felt like I finally got what I wanted: macOS merged with i3-like window tiling.<\/p>\n\n<p>For work on the laptop it\u2019s a godsend. Once you get used to working with a tiling window manager (I was already due to my time with i3) it\u2019s like a cheat code. Navigating around macOS feels so much quicker and snappier and I don\u2019t have to move my hands to the trackpad as often. It feels like a 50% speed improvement over regular swiping navigation.<\/p>\n\n<p>Together with a quick app launcher like <a href=\"https:\/\/www.raycast.com\/\">Raycast<\/a> working on a laptop is extremely efficient. It\u2019s so efficient that I also switched to working on a single monitor on my desk as I feel like it\u2019s much better and I can focus better on tasks when having quick access to everything with a keyboard shortcut away.<\/p>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>Go try it out, it\u2019s magic. Window tiling managers are your cheat code to a much more efficient and speedy navigation on macOS. It\u2019s extremely addictive and I\u2019d love for everyone to experience it. But I get that it\u2019s nerdy and not for everyone, so take my recommendation with a grain of salt.<\/p>\n\n<p>Cheers, Michael<\/p>","author":{"name":{}}},{"title":"Implementing Dynamic Search with Turbo Frames in Ruby on Rails 7","link":{"@attributes":{"href":"https:\/\/michaelwapp.com\/ruby\/rails\/bootstrap\/2024\/01\/27\/dynamic-search-rails\/","rel":"alternate","type":"text\/html","title":"Implementing Dynamic Search with Turbo Frames in Ruby on Rails 7"}},"published":"2024-01-27T04:00:00+00:00","updated":"2024-01-27T04:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2024-01-27-dynamic-search-rails.md","content":"<h3 id=\"introduction\">Introduction<\/h3>\n\n<p>In web development, a responsive and efficient search experience is key to user satisfaction. For my recent project at <a href=\"http:\/\/avimbu.com\">AVIMBU<\/a>, I implemented a search feature that queries both local data and external APIs. This was achieved using Hotwire\u2019s TurboFrames in a Ruby on Rails 7 web application.<\/p>\n\n<p>In this blog post, I\u2019ll guide you through implementing a dynamic search bar that fetches data both locally and from an external source (such as an API), all without a full page reload thanks to TurboFrames.<\/p>\n\n<h3 id=\"section-1-initial-project-setup\">Section 1: Initial Project Setup<\/h3>\n\n<p>As we are building a dynamic search implementation for a Ruby on Rails 7 project, we need a few steps to setup the initial project state. For this, run the following command:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>rails new dynamic_search --css=bootstrap -j=esbuild\n<\/code><\/pre><\/div><\/div>\n\n<p>This command creates a Rails project with the latest version of Bootstrap pre-configured (this is optional but provides better styling). For JavaScript packaging, we will use <code class=\"highlighter-rouge\">esbuild<\/code>, which is straightforward and simple to use. You could also opt for using <code class=\"highlighter-rouge\">webpacker<\/code> or the more modern <code class=\"highlighter-rouge\">import_maps<\/code> approach, however, for now it\u2019s easiest to stick with <code class=\"highlighter-rouge\">esbuild<\/code> due to its straightforward integration of Bootstrap.<\/p>\n\n<p>After project creation, navigate to the project directory and start it with:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>bin\/dev \n<\/code><\/pre><\/div><\/div>\n\n<p>Your application should now be available at <code class=\"highlighter-rouge\">localhost:3000<\/code>.<\/p>\n\n<p>This setup includes Turbo by default, a key component for dynamic page updates. Confirm its inclusion in <code class=\"highlighter-rouge\">app\/javascript\/application.js<\/code>:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>import \"@hotwired\/turbo-rails\"\n<\/code><\/pre><\/div><\/div>\n\n<h3 id=\"section-2-implementing-the-search-feature\">Section 2: Implementing the Search Feature<\/h3>\n\n<p>Begin by creating a Project model with <code class=\"highlighter-rouge\">name<\/code> and <code class=\"highlighter-rouge\">description<\/code> attributes. Use Rails\u2019 scaffolding to quickly set up the model and its CRUD operations:<\/p>\n\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>rails g scaffold project name description\n<\/code><\/pre><\/div><\/div>\n<p>Once that\u2019s done, you should be able to create, update and delete some project model entities within your application.<\/p>\n\n<p>Let\u2019s add the search function: I\u2019ve chosen the <code class=\"highlighter-rouge\">index<\/code> action of the ProjectsController. Therefore I\u2019ve added a search form to the corresponding view template:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">form_with<\/span> <span class=\"ss\">url: <\/span><span class=\"n\">projects_path<\/span><span class=\"p\">,<\/span> <span class=\"ss\">method: :get<\/span><span class=\"p\">,<\/span> <span class=\"ss\">data: <\/span><span class=\"p\">{<\/span> <span class=\"ss\">turbo_frame: <\/span><span class=\"s1\">'project_listings'<\/span> <span class=\"p\">}<\/span> <span class=\"k\">do<\/span> <span class=\"o\">|<\/span><span class=\"n\">form<\/span><span class=\"o\">|<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">form<\/span><span class=\"p\">.<\/span><span class=\"nf\">search_field<\/span> <span class=\"ss\">:q<\/span><span class=\"p\">,<\/span> <span class=\"ss\">class: <\/span><span class=\"s1\">'form-control'<\/span><span class=\"p\">,<\/span> <span class=\"ss\">placeholder: <\/span><span class=\"s2\">\"Search for Name or Description\"<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>This adds a simple search bar which allows us to submit a <code class=\"highlighter-rouge\">GET<\/code> search request against the <code class=\"highlighter-rouge\">projects#index<\/code> action. The search query is provided via the <code class=\"highlighter-rouge\">q<\/code> parameter. I\u2019ve adapted the <code class=\"highlighter-rouge\">index<\/code> action as follows:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">def<\/span> <span class=\"nf\">index<\/span>\n<span class=\"vi\">@projects<\/span> <span class=\"o\">=<\/span> <span class=\"no\">Project<\/span><span class=\"p\">.<\/span><span class=\"nf\">all<\/span>\n<span class=\"vi\">@search<\/span> <span class=\"o\">=<\/span> <span class=\"n\">params<\/span><span class=\"p\">[<\/span><span class=\"ss\">:q<\/span><span class=\"p\">]<\/span> \n<span class=\"vi\">@results<\/span> <span class=\"o\">=<\/span> <span class=\"k\">if<\/span> <span class=\"vi\">@search<\/span><span class=\"p\">.<\/span><span class=\"nf\">blank?<\/span>\n  <span class=\"p\">[]<\/span>\n<span class=\"k\">else<\/span>\n  <span class=\"no\">Project<\/span><span class=\"p\">.<\/span><span class=\"nf\">where<\/span><span class=\"p\">(<\/span><span class=\"s1\">'name LIKE :search OR description LIKE :search'<\/span><span class=\"p\">,<\/span> <span class=\"ss\">search: <\/span><span class=\"s2\">\"%<\/span><span class=\"si\">#{<\/span><span class=\"vi\">@search<\/span><span class=\"si\">}<\/span><span class=\"s2\">%\"<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>After this change, I have two new variables which are accessible within the <code class=\"highlighter-rouge\">index<\/code> view. <code class=\"highlighter-rouge\">@search<\/code> holds the search query and <code class=\"highlighter-rouge\">@results<\/code> holds an array of found Project models. For the sake of demonstration, I\u2019ve implemented the database search using a simple <code class=\"highlighter-rouge\">LIKE<\/code> based search query. This can and should be improved by using better implementations such as <a href=\"https:\/\/github.com\/Casecommons\/pg_search\">pg_search<\/a>.<\/p>\n\n<p>The <code class=\"highlighter-rouge\">app\/views\/projects\/index.html.erb<\/code> also needs some changes:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_frame_tag<\/span> <span class=\"s1\">'project_listings'<\/span> <span class=\"k\">do<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%<\/span> <span class=\"k\">unless<\/span> <span class=\"vi\">@results<\/span><span class=\"p\">.<\/span><span class=\"nf\">empty?<\/span> <span class=\"cp\">%&gt;<\/span>\n      <span class=\"cp\">&lt;%<\/span> <span class=\"vi\">@results<\/span><span class=\"o\">&amp;<\/span><span class=\"p\">.<\/span><span class=\"nf\">each<\/span> <span class=\"k\">do<\/span> <span class=\"o\">|<\/span><span class=\"n\">project<\/span><span class=\"o\">|<\/span> <span class=\"cp\">%&gt;<\/span>\n          <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"p-2 border\"<\/span><span class=\"nt\">&gt;<\/span>\n              <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"fs-6\"<\/span><span class=\"nt\">&gt;<\/span>Name: <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">project<\/span><span class=\"p\">.<\/span><span class=\"nf\">name<\/span> <span class=\"cp\">%&gt;<\/span> - Description: <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">project<\/span><span class=\"p\">.<\/span><span class=\"nf\">description<\/span> <span class=\"cp\">%&gt;<\/span><span class=\"nt\">&lt;\/div&gt;<\/span>\n          <span class=\"nt\">&lt;\/div&gt;<\/span>\n      <span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n<p>I\u2019ve wrapped the search results in a <code class=\"highlighter-rouge\">turbo_frame_tag<\/code> with the same ID as the form submits to. Therefore, Turbo is smart enough to figure out which part of the DOM needs to change and only updates the <code class=\"highlighter-rouge\">project_listings<\/code> <code class=\"highlighter-rouge\">turbo_frame_tag<\/code> to avoid a full page refresh.<\/p>\n\n<p>No, if the local DB search returns some results, the search results are shown without the need of a full page refresh.<\/p>\n\n<h3 id=\"section-3-integration-of-external-search\">Section 3: Integration of External Search<\/h3>\n\n<p>The first search part is now implemented. Now, let\u2019s add some external search functionality to incorporate an external API for my search results in case the local search doesn\u2019t provide the results I\u2019m looking for. For this, I\u2019m using the concept of lazy-loading <code class=\"highlighter-rouge\">turbo_frame_tags<\/code>. This allows me to trigger \/ load the source action of a TurboFrame as soon as the frame is visible in the viewport.<\/p>\n\n<p>I have to change three things to make it work. First, add the <code class=\"highlighter-rouge\">turbo_frame_tag<\/code> within the first search result TurboFrame s.t. it\u2019s always reloaded when the first TurboFrame is reloaded. Ensure that the <code class=\"highlighter-rouge\">src<\/code> option is pointing to the external search action, which we are now going to add. For this, add the following route to your <code class=\"highlighter-rouge\">routes.rb<\/code> file:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"n\">get<\/span> <span class=\"s1\">'projects\/external_search_project'<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"s1\">'projects#external_search_project'<\/span><span class=\"p\">,<\/span> <span class=\"ss\">as: :load_external_search_project<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Also, add this method (action) to the projects controller:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">def<\/span> <span class=\"nf\">external_search_project<\/span>\n  <span class=\"k\">return<\/span> <span class=\"k\">if<\/span> <span class=\"n\">params<\/span><span class=\"p\">[<\/span><span class=\"ss\">:q<\/span><span class=\"p\">].<\/span><span class=\"nf\">blank?<\/span> <span class=\"o\">||<\/span> <span class=\"n\">params<\/span><span class=\"p\">[<\/span><span class=\"ss\">:avoid_search<\/span><span class=\"p\">]<\/span> <span class=\"o\">==<\/span> <span class=\"s2\">\"true\"<\/span>\n\n  <span class=\"nb\">sleep<\/span> <span class=\"mf\">1.0<\/span> <span class=\"c1\"># simulate external request<\/span>\n  <span class=\"vi\">@external_search_result<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"no\">Project<\/span><span class=\"p\">.<\/span><span class=\"nf\">new<\/span><span class=\"p\">(<\/span><span class=\"ss\">name: <\/span><span class=\"s2\">\"External Project\"<\/span><span class=\"p\">,<\/span> <span class=\"ss\">description: <\/span><span class=\"s2\">\"Loaded from wherever\"<\/span><span class=\"p\">)]<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>It\u2019s an example implementation where the external search request is simulated using <code class=\"highlighter-rouge\">sleep<\/code>. Also, the action is skipped if no search query is passed or an <code class=\"highlighter-rouge\">avoid_search<\/code> param is given.<\/p>\n\n<p>Finally, add the external search TurboFrame inside the first TurboFrame to ensure that it\u2019s always loaded with a regular search update. By passing the search query and setting the <code class=\"highlighter-rouge\">avoid_search<\/code> parameter depending on whether local search results are available or not, the external search is only triggered accordingly. This allows for only executing and rendering external search results if local search didn\u2019t provide any results.<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_frame_tag<\/span> <span class=\"s1\">'project_listings'<\/span> <span class=\"k\">do<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%<\/span> <span class=\"k\">unless<\/span> <span class=\"vi\">@results<\/span><span class=\"p\">.<\/span><span class=\"nf\">empty?<\/span> <span class=\"cp\">%&gt;<\/span>\n      <span class=\"cp\">&lt;%<\/span> <span class=\"vi\">@results<\/span><span class=\"o\">&amp;<\/span><span class=\"p\">.<\/span><span class=\"nf\">each<\/span> <span class=\"k\">do<\/span> <span class=\"o\">|<\/span><span class=\"n\">project<\/span><span class=\"o\">|<\/span> <span class=\"cp\">%&gt;<\/span>\n          <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"p-2 border\"<\/span><span class=\"nt\">&gt;<\/span>\n              <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"fs-6\"<\/span><span class=\"nt\">&gt;<\/span>Name: <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">project<\/span><span class=\"p\">.<\/span><span class=\"nf\">name<\/span> <span class=\"cp\">%&gt;<\/span> - Description: <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">project<\/span><span class=\"p\">.<\/span><span class=\"nf\">description<\/span> <span class=\"cp\">%&gt;<\/span><span class=\"nt\">&lt;\/div&gt;<\/span>\n          <span class=\"nt\">&lt;\/div&gt;<\/span>\n      <span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_frame_tag<\/span> <span class=\"s1\">'external_search_result'<\/span><span class=\"p\">,<\/span> <span class=\"ss\">src: <\/span><span class=\"n\">load_external_search_project_path<\/span><span class=\"p\">(<\/span><span class=\"ss\">q: <\/span><span class=\"vi\">@search<\/span><span class=\"p\">,<\/span> <span class=\"ss\">avoid_search: <\/span><span class=\"vi\">@results<\/span><span class=\"p\">.<\/span><span class=\"nf\">any?<\/span><span class=\"p\">),<\/span> <span class=\"ss\">loading: :lazy<\/span> <span class=\"k\">do<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Also, to update the <code class=\"highlighter-rouge\">external_search_result<\/code> TurboFrame, add this view template to <code class=\"highlighter-rouge\">app\/views\/projects\/external_search_project.html.erb<\/code>. This will be rendered on external search and lists the external search results if they are available.<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_frame_tag<\/span> <span class=\"s2\">\"external_search_result\"<\/span> <span class=\"k\">do<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"vi\">@external_search_result<\/span><span class=\"o\">&amp;<\/span><span class=\"p\">.<\/span><span class=\"nf\">each<\/span> <span class=\"k\">do<\/span> <span class=\"o\">|<\/span><span class=\"n\">project<\/span><span class=\"o\">|<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"p-2 border\"<\/span><span class=\"nt\">&gt;<\/span>\n    <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"fs-6\"<\/span><span class=\"nt\">&gt;<\/span>Name: <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">project<\/span><span class=\"p\">.<\/span><span class=\"nf\">name<\/span> <span class=\"cp\">%&gt;<\/span> - Description: <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">project<\/span><span class=\"p\">.<\/span><span class=\"nf\">description<\/span> <span class=\"cp\">%&gt;<\/span><span class=\"nt\">&lt;\/div&gt;<\/span>\n  <span class=\"nt\">&lt;\/div&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>That\u2019s it. Now you have control over your search behavior. This allows you to adapt the search implementation to your needs.<\/p>\n\n<p>The final product should look like this:<\/p>\n\n<p><img src=\"\/images\/blog\/2024-01-27-dynamic-search-rails\/final_solution.gif\" alt=\"final_solution\" \/><\/p>\n\n<h3 id=\"section-4-improvement-form-auto-submission\">Section 4: Improvement: Form Auto-Submission<\/h3>\n\n<p>Enhance the search experience with automatic form submission. Use a Stimulus controller to submit the form after a brief delay. Attach the controller to the form as follows:<\/p>\n\n<div class=\"language-javascript highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">import<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">Controller<\/span> <span class=\"p\">}<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">\"<\/span><span class=\"s2\">@hotwired\/stimulus<\/span><span class=\"dl\">\"<\/span>\n\n<span class=\"k\">export<\/span> <span class=\"k\">default<\/span> <span class=\"kd\">class<\/span> <span class=\"nc\">extends<\/span> <span class=\"nx\">Controller<\/span> <span class=\"p\">{<\/span>\n\n<span class=\"nf\">search<\/span><span class=\"p\">()<\/span> <span class=\"p\">{<\/span>\n  <span class=\"nf\">clearTimeout<\/span><span class=\"p\">(<\/span><span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">timeout<\/span><span class=\"p\">)<\/span>\n  <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">timeout<\/span> <span class=\"o\">=<\/span> <span class=\"nf\">setTimeout<\/span><span class=\"p\">(()<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"p\">{<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">element<\/span><span class=\"p\">.<\/span><span class=\"nf\">requestSubmit<\/span><span class=\"p\">()<\/span>\n  <span class=\"p\">},<\/span> <span class=\"mi\">300<\/span><span class=\"p\">)<\/span>\n<span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre><\/div><\/div>\n<p>You also need to attach this controller to the search form like this:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">form_with<\/span> <span class=\"ss\">url: <\/span><span class=\"n\">projects_path<\/span><span class=\"p\">,<\/span> <span class=\"ss\">method: :get<\/span><span class=\"p\">,<\/span> <span class=\"ss\">data: <\/span><span class=\"p\">{<\/span> <span class=\"ss\">controller: <\/span><span class=\"s2\">\"formsubmission\"<\/span><span class=\"p\">,<\/span> <span class=\"ss\">turbo_frame: <\/span><span class=\"s1\">'project_listings'<\/span> <span class=\"p\">}<\/span> <span class=\"k\">do<\/span> <span class=\"o\">|<\/span><span class=\"n\">form<\/span><span class=\"o\">|<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">form<\/span><span class=\"p\">.<\/span><span class=\"nf\">search_field<\/span> <span class=\"ss\">:q<\/span><span class=\"p\">,<\/span> <span class=\"ss\">class: <\/span><span class=\"s1\">'form-control'<\/span><span class=\"p\">,<\/span> <span class=\"ss\">placeholder: <\/span><span class=\"s2\">\"Search for Name or Description\"<\/span><span class=\"p\">,<\/span> <span class=\"ss\">data: <\/span><span class=\"p\">{<\/span> <span class=\"ss\">action: <\/span><span class=\"s2\">\"input-&gt;formsubmission#search\"<\/span> <span class=\"p\">}<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Now, after this addition, the form submits automatically after 300ms.<\/p>\n\n<h3 id=\"conclusion\">Conclusion<\/h3>\n\n<p>Utilizing TurboFrames in this manner provides an effective solution for dynamic searching in Rails applications.<\/p>\n\n<h3 id=\"additional-resources\">Additional Resources<\/h3>\n\n<p>For more insights into TurboFrames and Hotwire, visit the <a href=\"https:\/\/turbo.hotwired.dev\/\">Hotwire Turbo documentation<\/a>.<\/p>\n\n<p>If you want to know more about me, feel free to check out my <a href=\"http:\/\/michaelwapp.com\">homepage<\/a> or follow me on Twitter <a href=\"https:\/\/twitter.com\/michael_wapp\">here<\/a>.<\/p>\n\n<h3 id=\"additional-resources-1\">Additional Resources<\/h3>\n\n<p>The code used in this tutorial can be found <a href=\"https:\/\/github.com\/michaelwapp\/dynamic_search\">here<\/a>.<\/p>\n\n<p>Alternative resources:<\/p>\n\n<ul>\n  <li><a href=\"https:\/\/guides.rubyonrails.org\/\">Ruby on Rails Guidelines<\/a><\/li>\n  <li><a href=\"https:\/\/hotwired.dev\/\">Rails Hotwire<\/a><\/li>\n<\/ul>","author":{"name":{}}},{"title":"Integrating Bootstrap 5 Modals with Ruby on Rails 7 Using Turbo Frames","link":{"@attributes":{"href":"https:\/\/michaelwapp.com\/ruby\/rails\/bootstrap\/2024\/01\/25\/bootstrap-5-modals\/","rel":"alternate","type":"text\/html","title":"Integrating Bootstrap 5 Modals with Ruby on Rails 7 Using Turbo Frames"}},"published":"2024-01-25T04:00:00+00:00","updated":"2024-01-25T04:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2024-01-25-bootstrap-5-modals.md","content":"<h2 id=\"introduction\">Introduction<\/h2>\n\n<p>In my journey to build a new <a href=\"https:\/\/guides.rubyonrails.org\/\">Ruby on Rails 7<\/a> app, I aim to create something as modern and reactive as a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Single-page_application\">Single Page Application (SPA)<\/a> written in React. The project I\u2019m currently working on, named <a href=\"http:\/\/avimbu.com\">AVIMBU<\/a>, can be explored further <a href=\"http:\/\/avimbu.com\">here<\/a>.<\/p>\n\n<p>A common feature in SPAs is the use of modals. My goal was to find a simple yet powerful and flexible approach to integrate Bootstrap modals without adding them permanently to the DOM, only revealing them upon a button click. Fortunately, <a href=\"https:\/\/turbo.hotwired.dev\/handbook\/frames\">Turbo Frames<\/a> offer a solution to transfer HTML over the wire, allowing us to display modals without rerendering the entire page.<\/p>\n\n<p>This blog post provides a step-by-step guide on integrating a <a href=\"https:\/\/getbootstrap.com\/docs\/5.3\/components\/modal\/#examples\">Bootstrap 5 modal<\/a> in a Ruby on Rails 7 application using Turbo Frames, thus enhancing your application\u2019s interactivity without full page reloads.<\/p>\n\n<p><img src=\"\/images\/blog\/2024-01-25-bootstrap-5-modals\/bootstrap_modal-2.webp\" alt=\"Bootstrap Modal\" title=\"Bootstrap Modal\" \/><\/p>\n\n<h2 id=\"ruby-on-rails\">Ruby On Rails<\/h2>\n\n<h3 id=\"section-1-initial-project-setup\">Section 1: Initial Project Setup<\/h3>\n\n<p>In order to create a barebones Rails 7 project with Bootstrap preconfigured, run the following command:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>rails new bootstrap_modal --css=bootstrap -j=esbuild\n<\/code><\/pre><\/div><\/div>\n\n<p>This command creates a Rails project with the latest version of Bootstrap preconfigured. For JavaScript packaging, we will use <code class=\"highlighter-rouge\">esbuild<\/code>, which is straightforward and simple to use. You could also opt for using <code class=\"highlighter-rouge\">webpacker<\/code> or the more modern <code class=\"highlighter-rouge\">import_maps<\/code> approach, however, for now it\u2019s easiest to stick with <code class=\"highlighter-rouge\">esbuild<\/code> due to its straightforward integration of Bootstrap.<\/p>\n\n<p>After project creation, navigate to the project directory and start it with:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>bin\/dev \n<\/code><\/pre><\/div><\/div>\n\n<p>Your application should now be available at <code class=\"highlighter-rouge\">localhost:3000<\/code>.<\/p>\n\n<p>This initial setup comes with Turbo enabled. Verify this by checking for the following line in <code class=\"highlighter-rouge\">app\/javascript\/application.js<\/code>:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>import \"@hotwired\/turbo-rails\"\n<\/code><\/pre><\/div><\/div>\n\n<h3 id=\"section-2-add-a-turbo-frame-tag\">Section 2: Add a Turbo Frame Tag<\/h3>\n\n<p>In order to achieve a reactive and modern-feeling web application, we\u2019ll utilize Turbo Frames. Turbo Frames, a key component of Hotwire, allow us to define which parts of an application\u2019s HTML should be replaced, rather than doing a full page reload.<\/p>\n\n<p>To use them, add the following line to a view file:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_frame_tag<\/span> <span class=\"s2\">\"modal\"<\/span><span class=\"p\">,<\/span> <span class=\"ss\">target: <\/span><span class=\"s2\">\"_top\"<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>I added this line to my <code class=\"highlighter-rouge\">application.html.erb<\/code> file, right above the <code class=\"highlighter-rouge\">&lt;%= yield %&gt;<\/code> part. This allows for adding my modal HTML element to this frame. The <code class=\"highlighter-rouge\">turbo_frame_tag<\/code> creates a <code class=\"highlighter-rouge\">div<\/code> element with the class set to <code class=\"highlighter-rouge\">modal<\/code>, which Turbo uses to replace the content of this div with the new Turbo Frame content. The <code class=\"highlighter-rouge\">target: \"_top\"<\/code> attribute allows for conventional redirects (e.g., on model creation).<\/p>\n\n<h3 id=\"section-3-add-a-modal-partial\">Section 3: Add a Modal Partial<\/h3>\n\n<p>From the Bootstrap documentation, you can find some skeleton Bootstrap 5 modal examples <a href=\"https:\/\/getbootstrap.com\/docs\/5.3\/components\/modal\/#examples\">here<\/a>. I used one of them and created a <code class=\"highlighter-rouge\">_modal.html.erb<\/code> partial for later reuse. Ensure to copy an \u201cactive\u201d modal instead of a hidden one. The partial looks like this:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_frame_tag<\/span> <span class=\"s2\">\"modal\"<\/span> <span class=\"k\">do<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"modal fade\"<\/span> <span class=\"na\">tabindex=<\/span><span class=\"s\">\"-1\"<\/span> <span class=\"na\">data-controller=<\/span><span class=\"s\">\"modal\"<\/span><span class=\"nt\">&gt;<\/span>\n    <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"modal-dialog modal-dialog-centered modal-lg\"<\/span><span class=\"nt\">&gt;<\/span>\n      <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"modal-content\"<\/span><span class=\"nt\">&gt;<\/span>\n        <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"modal-header\"<\/span><span class=\"nt\">&gt;<\/span>\n          <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class= <\/span><span class=\"s\">\"modal-title fs-3\"<\/span><span class=\"nt\">&gt;<\/span>\n            <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">title<\/span> <span class=\"cp\">%&gt;<\/span>\n          <span class=\"nt\">&lt;\/div&gt;<\/span>\n          <span class=\"nt\">&lt;button<\/span> <span class=\"na\">type=<\/span><span class=\"s\">\"button\"<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"btn-close\"<\/span> <span class=\"na\">data-bs-dismiss=<\/span><span class=\"s\">\"modal\"<\/span> <span class=\"na\">aria-label=<\/span><span class=\"s\">\"Close\"<\/span><span class=\"nt\">&gt;&lt;\/button&gt;<\/span>\n        <span class=\"nt\">&lt;\/div&gt;<\/span>\n        <span class=\"nt\">&lt;div<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"modal-body\"<\/span> <span class=\"na\">id=<\/span><span class=\"s\">\"remote_modal_body\"<\/span> <span class=\"nt\">&gt;<\/span>\n          <span class=\"cp\">&lt;%=<\/span> <span class=\"k\">yield<\/span> <span class=\"cp\">%&gt;<\/span>\n        <span class=\"nt\">&lt;\/div&gt;<\/span>\n      <span class=\"nt\">&lt;\/div&gt;<\/span>\n    <span class=\"nt\">&lt;\/div&gt;<\/span>\n  <span class=\"nt\">&lt;\/div&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<h3 id=\"section-4-stimulus-controller-for-background-handling\">Section 4: Stimulus Controller for Background Handling<\/h3>\n\n<p>Since the Bootstrap modal relies on some JavaScript logic, I added a few lines of JavaScript to ensure proper background handling. For this, I chose to use a Stimulus controller as it\u2019s the easiest solution for handling JavaScript within a modern Rails 7 application with Turbo. The controller looks like this:<\/p>\n\n<div class=\"language-javascript highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">import<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">Controller<\/span> <span class=\"p\">}<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">\"<\/span><span class=\"s2\">@hotwired\/stimulus<\/span><span class=\"dl\">\"<\/span>\n<span class=\"k\">import<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">Modal<\/span> <span class=\"p\">}<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">\"<\/span><span class=\"s2\">bootstrap<\/span><span class=\"dl\">\"<\/span><span class=\"p\">;<\/span>\n\n<span class=\"c1\">\/\/ Connects to data-controller=\"modal\"<\/span>\n<span class=\"k\">export<\/span> <span class=\"k\">default<\/span> <span class=\"kd\">class<\/span> <span class=\"nc\">extends<\/span> <span class=\"nx\">Controller<\/span> <span class=\"p\">{<\/span>\n  <span class=\"nf\">connect<\/span><span class=\"p\">()<\/span> <span class=\"p\">{<\/span>\n    <span class=\"kd\">let<\/span> <span class=\"nx\">backdrop<\/span> <span class=\"o\">=<\/span> <span class=\"nb\">document<\/span><span class=\"p\">.<\/span><span class=\"nf\">querySelector<\/span><span class=\"p\">(<\/span><span class=\"dl\">\"<\/span><span class=\"s2\">.modal-backdrop<\/span><span class=\"dl\">\"<\/span><span class=\"p\">);<\/span>\n    <span class=\"k\">if <\/span><span class=\"p\">(<\/span><span class=\"nx\">backdrop<\/span><span class=\"p\">)<\/span> <span class=\"p\">{<\/span>\n      <span class=\"nx\">backdrop<\/span><span class=\"p\">.<\/span><span class=\"nf\">remove<\/span><span class=\"p\">();<\/span>\n    <span class=\"p\">}<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">modal<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nc\">Modal<\/span><span class=\"p\">(<\/span><span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">element<\/span><span class=\"p\">);<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">modal<\/span><span class=\"p\">.<\/span><span class=\"nf\">show<\/span><span class=\"p\">();<\/span>\n    <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">element<\/span><span class=\"p\">.<\/span><span class=\"nf\">addEventListener<\/span><span class=\"p\">(<\/span><span class=\"dl\">\"<\/span><span class=\"s2\">hidden.bs.modal<\/span><span class=\"dl\">\"<\/span><span class=\"p\">,<\/span> <span class=\"p\">(<\/span><span class=\"nx\">event<\/span><span class=\"p\">)<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"p\">{<\/span>\n      <span class=\"k\">this<\/span><span class=\"p\">.<\/span><span class=\"nx\">element<\/span><span class=\"p\">.<\/span><span class=\"nf\">remove<\/span><span class=\"p\">();<\/span>\n    <span class=\"p\">});<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>As seen in my <code class=\"highlighter-rouge\">_modal.html.erb<\/code> partial, I\u2019ve added the <code class=\"highlighter-rouge\">data-controller=\"modal\"<\/code> parameter to link this Stimulus controller with the modal HTML. This controller ensures that the modal backdrop is handled correctly by properly removing and showing the Modal element.<\/p>\n\n<h3 id=\"section-5-adapt-controller-actions-to-use-turbo-frames\">Section 5: Adapt Controller Actions to Use Turbo Frames<\/h3>\n\n<p>Here\u2019s the crucial part of integrating Turbo Frames into our Controller actions. I created a simple example scaffold for some model logic:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>rails g scaffold project name description\n<\/code><\/pre><\/div><\/div>\n\n<p>This command creates all necessary controllers, models, and views to create, update, or delete Project models. Change your root route to the index action of the Project controller by adding this to <code class=\"highlighter-rouge\">routes.rb<\/code>:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"n\">root<\/span> <span class=\"s2\">\"projects#index\"<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Now, when you open your application, you\u2019ll see the index page of the Project model, allowing you to create some Project models. However, the new and edit actions redirect to their respective <code class=\"highlighter-rouge\">.html.erb<\/code> pages. We want them to open within a modal. This can be done by adapting the view files as follows:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">render<\/span> <span class=\"s1\">'partials\/modal'<\/span><span class=\"p\">,<\/span> <span class=\"ss\">title: <\/span><span class=\"s2\">\"New Project\"<\/span><span class=\"p\">,<\/span> <span class=\"ss\">fade_in: <\/span><span class=\"kp\">true<\/span> <span class=\"k\">do<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">render<\/span> <span class=\"s2\">\"form\"<\/span><span class=\"p\">,<\/span> <span class=\"ss\">project: <\/span><span class=\"vi\">@project<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Do the same for the <code class=\"highlighter-rouge\">edit.html.erb<\/code> file. Now, when you try to create or edit a model, the view renders within a Bootstrap modal \ud83e\udd73<\/p>\n\n<h3 id=\"section-6-add-form-error-handling\">Section 6: Add Form Error Handling<\/h3>\n\n<p>If you add model validations, such as requiring a name, you might notice that errors are not properly handled within the modal.<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class<\/span> <span class=\"nc\">Project<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">ApplicationRecord<\/span>  \n  <span class=\"n\">validates<\/span> <span class=\"ss\">:name<\/span><span class=\"p\">,<\/span> <span class=\"ss\">presence: <\/span><span class=\"kp\">true<\/span>  \n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>In order to improve this, we need to change how the controller responds in case of an error for both the create and update actions:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># POST \/projects or \/projects.json  <\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">create<\/span>  \n  <span class=\"vi\">@project<\/span> <span class=\"o\">=<\/span> <span class=\"no\">Project<\/span><span class=\"p\">.<\/span><span class=\"nf\">new<\/span><span class=\"p\">(<\/span><span class=\"n\">project_params<\/span><span class=\"p\">)<\/span>  \n  \n  <span class=\"k\">if<\/span> <span class=\"vi\">@project<\/span><span class=\"p\">.<\/span><span class=\"nf\">save<\/span>  \n    <span class=\"n\">redirect_to<\/span> <span class=\"n\">project_url<\/span><span class=\"p\">(<\/span><span class=\"vi\">@project<\/span><span class=\"p\">),<\/span> <span class=\"ss\">notice: <\/span><span class=\"s2\">\"Project was successfully created.\"<\/span>  \n  <span class=\"k\">else<\/span>  \n    <span class=\"n\">render<\/span> <span class=\"ss\">:form_update<\/span><span class=\"p\">,<\/span> <span class=\"ss\">status: :unprocessable_entity<\/span>  \n  <span class=\"k\">end<\/span>  \n<span class=\"k\">end<\/span>  \n  \n<span class=\"c1\"># PATCH\/PUT \/projects\/1 or \/projects\/1.json  <\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">update<\/span>  \n  <span class=\"k\">if<\/span> <span class=\"vi\">@project<\/span><span class=\"p\">.<\/span><span class=\"nf\">update<\/span><span class=\"p\">(<\/span><span class=\"n\">project_params<\/span><span class=\"p\">)<\/span>  \n    <span class=\"n\">redirect_to<\/span> <span class=\"n\">project_url<\/span><span class=\"p\">(<\/span><span class=\"vi\">@project<\/span><span class=\"p\">),<\/span> <span class=\"ss\">notice: <\/span><span class=\"s2\">\"Project was successfully updated.\"<\/span>  \n  <span class=\"k\">else<\/span>  \n    <span class=\"n\">render<\/span> <span class=\"ss\">:form_update<\/span><span class=\"p\">,<\/span> <span class=\"ss\">status: :unprocessable_entity<\/span>  \n  <span class=\"k\">end<\/span>  \n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>This adds a custom <code class=\"highlighter-rouge\">render<\/code> response for error cases, instructing the controller to render the <code class=\"highlighter-rouge\">form_update<\/code> view. This view will update the modal\u2019s body via a Turbo Stream (see <a href=\"https:\/\/turbo.hotwired.dev\/handbook\/streams\">Turbo Streams documentation<\/a>) to show the correct error and avoid a full page reload. The <code class=\"highlighter-rouge\">form_update.turbo_stream.erb<\/code> file looks like this:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_stream<\/span><span class=\"p\">.<\/span><span class=\"nf\">update<\/span> <span class=\"s2\">\"remote_modal_body\"<\/span> <span class=\"k\">do<\/span> <span class=\"cp\">%&gt;<\/span>\n  <span class=\"cp\">&lt;%=<\/span> <span class=\"n\">render<\/span> <span class=\"s2\">\"form\"<\/span><span class=\"p\">,<\/span> <span class=\"ss\">project: <\/span><span class=\"vi\">@project<\/span> <span class=\"cp\">%&gt;<\/span>\n<span class=\"cp\">&lt;%<\/span> <span class=\"k\">end<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Now, if you try to save a Project model without a name, the error is shown within the modal:<\/p>\n\n<p><img src=\"\/images\/blog\/2024-01-25-bootstrap-5-modals\/bootstrap_modal-3.webp\" alt=\"Modal with Error\" title=\"Modal with Error\" \/>\n<strong>Modal with Error<\/strong><\/p>\n\n<p>That\u2019s it! We\u2019ve successfully implemented a Bootstrap modal using Turbo Frames, making our Rails 7 application feel more responsive and modern.<\/p>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>The final product should look like this:<\/p>\n\n<p><img src=\"\/images\/blog\/2024-01-25-bootstrap-5-modals\/bootstrap_modal-4.gif\" alt=\"Final Product\" title=\"Final Product\" \/>\n<strong>Final Product<\/strong><\/p>\n\n<p>We\u2019ve covered how to integrate a Bootstrap 5 modal in a Ruby on Rails 7 application using Turbo Frames. This integration allows for a more dynamic and responsive user interface without the need for full page reloads. Experiment with Turbo Frames and Bootstrap modals, and feel free to share your experiences or ask questions in the comments below.<\/p>\n\n<p>If you want to know more about me, feel free to check out my <a href=\"http:\/\/michaelwapp.com\">homepage<\/a> or follow me on Twitter <a href=\"https:\/\/twitter.com\/michael_wapp\">here<\/a>.<\/p>\n\n<p><img src=\"\/images\/blog\/2024-01-25-bootstrap-5-modals\/bootstrap_modal-5.webp\" alt=\"Bootstrap 5\" title=\"Bootstrap 5\" \/>\n<strong>Bootstrap 5 - a developer friendly CSS component library<\/strong><\/p>\n\n<h3 id=\"additional-resources\"><strong>Additional Resources<\/strong><\/h3>\n\n<p>The code used in this tutorial can be found <a href=\"https:\/\/github.com\/michaelwapp\/bootstrap_modal_turbo_frame\">here<\/a>.<\/p>\n\n<p><strong>Alternative resources:<\/strong><\/p>\n\n<ul>\n  <li><a href=\"https:\/\/guides.rubyonrails.org\/\">Ruby on Rails Guidelines<\/a><\/li>\n  <li>Bootstrap <a href=\"https:\/\/getbootstrap.com\/\">documentation<\/a><\/li>\n  <li><a href=\"https:\/\/hotwired.dev\/\">Rails Hotwire<\/a><\/li>\n<\/ul>","author":{"name":{}}},{"title":"Setup Dokku (a free Heroku alternative) and Deploy a Ruby on Rails 7 Application","link":{"@attributes":{"href":"https:\/\/michaelwapp.com\/platform-as-a\/rails\/bootstrap\/2023\/08\/20\/dokku-setup\/","rel":"alternate","type":"text\/html","title":"Setup Dokku (a free Heroku alternative) and Deploy a Ruby on Rails 7 Application"}},"published":"2023-08-20T04:00:00+00:00","updated":"2023-08-20T04:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2023-08-20-dokku-setup.md","content":"<h2 id=\"create-your-own-heroku-clone-by-using-dokku\">Create your own Heroku clone by using Dokku<\/h2>\n\n<p><a href=\"https:\/\/www.heroku.com\/\">Heroku<\/a> was a popular solution for quick and easy deployments for Ruby on Rails developers. It was handy for testing and learning purposes with its free tier option, offering Platform as a Service (PaaS) solutions for everyone. Unfortunately, <a href=\"https:\/\/www.heroku.com\/\">Heroku<\/a> dropped its free tier solution, making it necessary to pay for every service offered by Heroku. On the hunt for a new solution to quickly and easily deploy Ruby on Rails applications, I found a Heroku alternative called <a href=\"https:\/\/dokku.com\/\">Dokku<\/a>.<\/p>\n\n<p>Dokku is a free open-source Platform as a Service solution that allows you to build your own Heroku clone on your server. It allows you to do git push-based deployments directly to your own Dokku instance running on your server with useful auto-deployment features. Dokku supports pretty much anything which could have been deployed to Heroku.<\/p>\n\n<p>The main use case I\u2019m covering with Dokku is for testing and development, however, I don\u2019t see any problems running smaller applications also in production using Dokku.<\/p>\n\n<p>This guide shows you how to set up Dokku on your server and explains how to deploy and configure a Ruby on Rails 7 application (including Postgres, Let\u2019s Encrypt and Database Encryption) as well as how to work with your application (executing commands, logging and configuration).<\/p>\n\n<h2 id=\"prerequisites\">Prerequisites<\/h2>\n\n<h3 id=\"what-do-you-need\">What do you need?<\/h3>\n\n<ul>\n  <li>A VPS (Virtual Private Server) or dedicated server with a fresh Ubuntu 20.04 installation with at least 1GB of RAM.<\/li>\n  <li>(Optional) Domain name pointing to your server\u2019s IP address.<\/li>\n  <li>Basic familiarity with the Linux command line as well as SSH.<\/li>\n<\/ul>\n\n<h2 id=\"part-1---install-and-setup-dokku\">Part 1 - Install and Setup Dokku<\/h2>\n\n<p>The first part of this article explains how to install and set up Dokku on your server. For this, the following steps have to be executed:<\/p>\n\n<ul>\n  <li>SSH into your server as the root user:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  ssh root@your\\_server\\_ip\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Download and run the Dokku installation script:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  wget -NP . https:\/\/dokku.com\/install\/v0.31.0\/bootstrap.sh  \n  sudo DOKKU\\_TAG=v0.31.0 bash bootstrap.sh\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Add your domain to Dokku (\u201cacme.com\u201d is your domain). You also have to add an A or CNAME record in your domain\u2019s DNS settings pointing to your server\u2019s IP address.<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku domains:set-global acme.com\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Alternatively, you can just use your server\u2019s IP address directly:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku domains:set-global &lt;your servers IP address&gt;  \n  dokku domains:set-global &lt;your servers IP address&gt; # add `.sslip.io` for subdomain support\n<\/code><\/pre><\/div><\/div>\n\n<p>That\u2019s pretty much it for the Dokku setup. After these steps, your Dokku instance is ready for its first deployment. If you need more instructions, check their excellent documentation <a href=\"https:\/\/dokku.com\/docs\/getting-started\/installation\/\">here<\/a>.<\/p>\n\n<h2 id=\"part-2---ruby-on-rails-7-application-deployment\">Part 2 - Ruby on Rails 7 Application Deployment<\/h2>\n\n<p>You can skip the first two steps if you already have an existing application. If not, follow along.<\/p>\n\n<ul>\n  <li>Create a new Ruby on Rails application (it\u2019s assumed that you have your system setup for Rails 7 development):<\/li>\n<\/ul>\n\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  rails new myapp <span class=\"o\">&amp;&amp;<\/span> <span class=\"nb\">cd <\/span>myapp\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Create your first commit<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  git add .   \n  git commit -m \"chore: Initial commit\"\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>To deploy the application to your Dokku server on git push, you simply need to add your server as a new remote git server. This can be done by running the following command:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  git remote add dokku dokku@&lt;global dokku domain or IP address&gt;:myapp\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>After this, you can simply push your application to your Dokku instance:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  git push dokku master\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>By doing this, Dokku will automatically detect that you are pushing a Ruby on Rails application and start the deployment process.<\/li>\n<\/ul>\n\n<p>After a successful deployment, you can access your Rails application by visiting: <a href=\"http:\/\/your-domain.com\">http:\/\/your-domain.com<\/a>. Dokku also prints the deployment log to your terminal so you can follow along. In the end, the URL of your deployed application is printed for you to access your application.<\/p>\n\n<h2 id=\"part-3---database-configuration\">Part 3 - Database Configuration<\/h2>\n\n<p>To get your database set up correctly the following steps are necessary. For the sake of this tutorial, it\u2019s assumed that you use Postgres as your production database.<\/p>\n\n<ul>\n  <li>Update your database configuration in your Ruby on Rails project. Dokku exports a DATABASE_URL environment variable which you can use to set up the configuration properly. Therefore, adapt your \u201cdatabase.yml\u201d configuration file with the following lines:<\/li>\n<\/ul>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  <span class=\"ss\">production:  \n    adapter: <\/span><span class=\"n\">postgresql<\/span>  \n    <span class=\"ss\">encoding: <\/span><span class=\"n\">unicode<\/span>  \n    <span class=\"ss\">url:   \n    pool: <\/span><span class=\"mi\">5<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Next, SSH into your Dokku server and install Postgres plugin for Dokku on your server:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku plugin:install https:\/\/github.com\/dokku\/dokku-postgres.git\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>After that, you can simply create and link a new Postgres database by running this:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku postgres:create mydatabase  \n  dokku postgres:link mydatabase myapp\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Now your application deployment can talk to your Postgres database. The linking command automatically restarts the deployment for you s.t. the Rails application is correctly configured.<\/li>\n<\/ul>\n\n<p>Additionally, if you work with the Rails migration system, it\u2019s wise to set up an automatic migration command for each deployment. For this, add an \u201capp.json\u201d file to your Rails project\u2019s root path, containing the following lines:<\/p>\n\n<div class=\"language-json highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"p\">{<\/span><span class=\"w\">  \n<\/span><span class=\"nl\">\"name\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"&lt;Name of Application&gt;\"<\/span><span class=\"p\">,<\/span><span class=\"w\">  \n<\/span><span class=\"nl\">\"description\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"&lt;Description of your application&gt;\"<\/span><span class=\"p\">,<\/span><span class=\"w\">  \n<\/span><span class=\"nl\">\"scripts\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"w\">  \n  <\/span><span class=\"nl\">\"dokku\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"w\">  \n    <\/span><span class=\"nl\">\"postdeploy\"<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">\"bundle exec rake db:migrate\"<\/span><span class=\"w\">  \n  <\/span><span class=\"p\">}<\/span><span class=\"w\">  \n<\/span><span class=\"p\">}<\/span><span class=\"w\">  \n<\/span><span class=\"p\">}<\/span><span class=\"w\">\n<\/span><\/code><\/pre><\/div><\/div>\n\n<p>This will automatically execute the migration command after every deployment. You can also configure any other command you might need here. Commit and push this file and your deployment should execute the migration automatically.<\/p>\n\n<p>If you want to use Rails <a href=\"https:\/\/guides.rubyonrails.org\/active_record_encryption.html\">Active Record Encryption<\/a>, you need to configure your Rails deployment with the \u201cRAILS_MASTER_KEY\u201d, containing the content of the \u201cmaster.key\u201d file. This can be done by adding the environment variable to the deployment by using the following Dokku command:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>dokku config:set &lt;name-of-application&gt; RAILS\\_MASTER\\_KEY=&lt;content of master.key&gt;  \ndokku config:show &lt;name-of-application&gt; # shows all environment variables set for the application\n<\/code><\/pre><\/div><\/div>\n\n<p>With this command, it\u2019s also possible to set up any other environment variable you might need in production.<\/p>\n\n<h2 id=\"part-4---redis\">Part 4 - Redis<\/h2>\n\n<p>If your Rails application uses Redis as a cache or session store, Dokku offers a solution for that. As with the Postgres setup, you need to install a plugin and configure some environment variables:<\/p>\n\n<ul>\n  <li>SSH into your server and install the Redis plugin<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku plugin:install https:\/\/github.com\/dokku\/dokku-redis.git redis \n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Create a Redis instance and link it to your application by running:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku redis:create myredis  \n  dokku redis:link myredis myapp\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>The Redis plugin exposes a \u201cREDIS_URL\u201d environment variable to add to your production Redis configuration. Add the environment variable so your Rails application knows where to find your Redis instance. Update your application configuration accordingly and push the changes to Dokku. Your production deployment should pick up the connection to the Redis instance automatically and work as expected.<\/li>\n<\/ul>\n\n<h2 id=\"part-5-ssl--lets-encrypt-setup\">Part 5\u2014 SSL &amp; Let\u2019s Encrypt Setup<\/h2>\n\n<p>If you need SSL for your application, Dokku has a Let\u2019s Encrypt plugin you can easily integrate. For this, the following steps are necessary.<\/p>\n\n<ul>\n  <li>SSH into your server and install the plugin by running this command:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku plugin:install https:\/\/github.com\/dokku\/dokku-letsencrypt.git \n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Configure your application with the Let\u2019s Encrypt email:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku config:set --no-restart &lt;appname&gt; DOKKU\\_LETSENCRYPT\\_EMAIL=your@email.tld\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>Add the plugin to your application<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku letsencrypt myapp\n<\/code><\/pre><\/div><\/div>\n\n<ul>\n  <li>That should be all. If you want auto-renewal of your certificates, you can add an easy cron-job script with the following command:<\/li>\n<\/ul>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>  dokku letsencrypt:cron-job --add myapp\n<\/code><\/pre><\/div><\/div>\n\n<p>Now you are all set and your application should be accessible via HTTPS.<\/p>\n\n<h2 id=\"tips-and-tricks\">Tips and Tricks<\/h2>\n\n<ul>\n  <li>If you want to avoid the SSH connection to your server, Dokku offers the option to run your remote commands from your local command line. For this, you need to install the official Dokku client and configure it with your server\u2019s connection information. For this, see the following <a href=\"https:\/\/dokku.com\/docs\/deployment\/remote-commands\/\">guide<\/a>.<\/li>\n  <li>I encountered a problem with the first deployment of my Ruby on Rails application with a mismatch of the bundler version. This is and was a common problem also for Heroku. You can simply fix this by updating your local bundler version and reinstalling bundler. See this Stackoverflow thread for more information: <a href=\"https:\/\/stackoverflow.com\/questions\/56774854\/how-do-i-fix-a-bundler-conflict-when-pushing-to-heroku\">Link<\/a>.<\/li>\n  <li>Dokku recommends at least 1GB of RAM for your server infrastructure. From personal experience, if you have a smaller Rails application with a DB connection I recommend at least 4GB of RAM. This ensures proper performance.<\/li>\n<\/ul>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>The experience of setting up Dokku and deploying a Ruby on Rails 7 application is pretty seamless. It offers everything you expect from a PaaS solution and can be easily configured and extended to your application needs. This tutorial covers the basic configuration as well as the deployment instructions necessary.<\/p>\n\n<p>For further information, check the reference links down below.<\/p>\n\n<p>Happy Coding<\/p>\n\n<p>Michael<\/p>\n\n<p>Thanks for reading this article. If you want, follow me on <a href=\"https:\/\/twitter.com\/michael_wapp\">Twitter<\/a>.<\/p>\n\n<p><strong>Related Links<\/strong><\/p>\n\n<ul>\n  <li><a href=\"https:\/\/dokku.com\/docs\/getting-started\/installation\/\">https:\/\/dokku.com\/docs\/getting-started\/installation\/<\/a><\/li>\n  <li><a href=\"https:\/\/github.com\/dokku\/dokku-letsencrypt\">https:\/\/github.com\/dokku\/dokku-letsencrypt<\/a><\/li>\n  <li><a href=\"https:\/\/github.com\/dokku\/dokku-postgres\">https:\/\/github.com\/dokku\/dokku-postgres<\/a><\/li>\n  <li><a href=\"https:\/\/github.com\/dokku\/dokku-redis\">https:\/\/github.com\/dokku\/dokku-redis<\/a><\/li>\n  <li><a href=\"https:\/\/guides.rubyonrails.org\/getting_started.html\">https:\/\/guides.rubyonrails.org\/getting_started.html<\/a><\/li>\n<\/ul>","author":{"name":{}}}]}