
I was working on a Rails application—standard CRUD with a sidebar and a few interactive menus. Everything worked on first load. Then I navigated away and came back. The sidebar was gone. I opened a dropdown, clicked a Turbo link, hit the back button. The dropdown was still open, sitting there on top of a page that had already moved on.

If you've built anything with Turbo and Stimulus beyond basic forms, you've likely seen this. Components work fine on full page loads, but Turbo introduces a different lifecycle. Pages get cached mid-state, morphs overwrite client-side changes with stale server HTML, and your UI ends up stuck in states it should have left behind.

Fixing this in [Maquina Components](/documentation/components/) is what version 0.4.0 is about.

## The Teardown Pattern

The core problem is described well by [Better Stimulus](https://betterstimulus.com/turbo/teardown). When Turbo navigates away from a page, it takes a snapshot of the DOM before leaving. When the user returns, Turbo shows that snapshot first. Any DOM changes your Stimulus controllers made—open menus, expanded panels, loading classes—get frozen into the cache.

The standard Stimulus `disconnect` callback handles general cleanup, but it doesn't distinguish between "the element was removed from the DOM" and "Turbo is about to cache this page." You need both.

The Teardown pattern adds a `teardown` method to controllers, triggered by Turbo's `turbo:before-cache` event. Every controller that manipulates the DOM can opt in, resetting its visual state before Turbo takes the snapshot. This keeps `disconnect` clean for general lifecycle concerns and gives Turbo-specific rollback its own dedicated path.

This release applies that pattern across the interactive components in the library.

## Sidebar: Three Problems at Once

The sidebar was the hardest to get right. It had three separate issues interacting with each other.

**Random IDs broke morphing.** The sidebar generated IDs like `sidebar-a3f9b2` on every render. Turbo's idiomorph algorithm matches elements by ID—when the ID changes every time, idiomorph can't find the element and treats it as new. Every morph was destroying and recreating the sidebar from scratch. The fix: deterministic IDs. `sidebar-left` and `sidebar-right`, consistent across renders. The sidebar provider also gets a stable ID (`sidebar-provider` by default).

**Morphs overwrote client state.** The sidebar stores its open/closed state in a cookie so it persists across page loads. During a Turbo morph, the server sends back HTML with the default state—it doesn't know about the cookie. Idiomorph applies the server HTML, and the sidebar collapses even though the user had it open.

The fix adds a `turbo:before-morph-element` listener with a `_morphing` guard flag. When a morph happens, the controller reads the cookie (the source of truth on the client), reasserts the correct state, and strips the `sidebar-loading` class that the server HTML reintroduces.

**Layout shift on desktop.** When Stimulus initialized and switched the sidebar from its mobile offcanvas mode to the desktop collapsible mode, there was a visible jump. The transition happened after the browser had already painted. This release smooths that handoff so the mode switch doesn't cause a flash.

## The Yield Trap

The second category of fixes has nothing to do with Turbo. It's a Rails rendering behavior that caught me off guard.

Nine partials in the library used the standard block pattern:

```erb
<%%= render "components/card/description" do %>
  <p>Custom HTML</p>
<%% end %>
```

This works when you always pass a block. But render the partial *without* a block and `yield` inside it doesn't return nothing—it renders the entire page's content into the partial. Rails treats the missing block as a signal to yield the page-level content instead.

The result: components rendering the full page body inside a card title or a toast message. It only shows up in specific usage patterns, and when it does, the output looks completely wrong with no obvious cause.

The fix replaces `yield` with an explicit `content:` parameter in all nine affected partials:

- card/title, card/description
- alert/title, alert/description
- toast/title, toast/description
- combobox/label, toast (main), toaster

The five toast helper methods no longer accept blocks either.

## Breaking Changes

This is a minor version bump with breaking changes:

- **Block syntax removed** for the 9 partials listed above. Use `content: capture { ... }` instead of `do ... end`.
- **Toast helpers** no longer accept blocks. Use the `content:` parameter.
- **Sidebar IDs** changed from `sidebar-<random_hex>` to `sidebar-left` / `sidebar-right`.
- **Sidebar provider** now has a stable `id` attribute (`sidebar-provider` by default).

## Migration

The content parameter change is mechanical. Find every block-style call to the affected partials and wrap the content with `capture`:

```erb
<%# Before %>
<%%= render "components/card/description" do %>
  <p>Custom HTML</p>
<%% end %>

<%# After %>
<%%= render "components/card/description",
      content: capture { %>
  <p>Custom HTML</p>
<%% } %>
```

For sidebar IDs, if you reference specific sidebar element IDs in JavaScript or tests, update them to `sidebar-left` or `sidebar-right`.

## Upgrading

```bash
bundle update maquina_components
```

## What This Reinforced

Turbo is not a transparent layer over page loads. It's a different execution model. Any Stimulus controller that touches the DOM needs to account for caching, morphing, and the gap between what the server renders and what the client has changed since. The [Teardown pattern](https://betterstimulus.com/turbo/teardown) should be the default starting point for any controller that does more than read values.

The `yield` behavior in Rails partials was a genuine surprise. It's documented, but it's a quiet trap when you have optional block content. Explicit parameters are safer.

## Documentation

- [Component Documentation](https://maquina.app/documentation/components/)
- [Sidebar](https://maquina.app/documentation/components/sidebar/)
- [Card](https://maquina.app/documentation/components/card/)
- [Alert](https://maquina.app/documentation/components/alert/)
- [Toast](https://maquina.app/documentation/components/toast/)
- [Combobox](https://maquina.app/documentation/components/combobox/)

## Source

- [Maquina Components](https://github.com/maquina-app/maquina_components)
- [Full Changelog](https://github.com/maquina-app/maquina_components/compare/v0.3.1.1...v0.4.0)
- [Better Stimulus: Teardown Pattern](https://betterstimulus.com/turbo/teardown)
