<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marvin Ahlgrimm</title>
    <description>The latest articles on DEV Community by Marvin Ahlgrimm (@treagod).</description>
    <link>https://dev.to/treagod</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1371028%2F9310213f-5f04-4d70-a2b9-684e9025bd14.jpeg</url>
      <title>DEV Community: Marvin Ahlgrimm</title>
      <link>https://dev.to/treagod</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/treagod"/>
    <language>en</language>
    <item>
      <title>Do AI Coding Agents Need a Different CSS Architecture?</title>
      <dc:creator>Marvin Ahlgrimm</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:25:12 +0000</pubDate>
      <link>https://dev.to/treagod/do-ai-coding-agents-need-a-different-css-architecture-4ni8</link>
      <guid>https://dev.to/treagod/do-ai-coding-agents-need-a-different-css-architecture-4ni8</guid>
      <description>&lt;h1&gt;
  
  
  Does BEM Still Make Sense When AI Writes CSS?
&lt;/h1&gt;

&lt;p&gt;I use BEM. Sometimes.&lt;/p&gt;

&lt;p&gt;Most of my CSS is plain CSS, with a main stylesheet and a few additional files for specific areas, such as &lt;code&gt;blog.css&lt;/code&gt;. The HTML is rendered on the server using &lt;a href="https://martenframework.com/" rel="noopener noreferrer"&gt;Marten&lt;/a&gt;. During deployment, a minifier takes care of the CSS, and Marten's &lt;code&gt;collectassets&lt;/code&gt; handles fingerprinting.&lt;/p&gt;

&lt;p&gt;That's pretty much my entire CSS toolchain. And I like it. There's very little overhead, and it does everything I currently need.&lt;/p&gt;

&lt;p&gt;But with AI coding agents writing more of my code, I started wondering whether my approach still made sense. Should I use BEM more consistently? Switch to data attributes for modifiers? Or consider something like Tailwind or CSS Modules?&lt;/p&gt;

&lt;p&gt;I ended up asking a Codex agent whether replacing BEM modifiers with &lt;code&gt;data-*&lt;/code&gt; attributes would improve an existing project.&lt;/p&gt;

&lt;p&gt;Its answer was rather simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;data-*&lt;/code&gt; doesn't automatically make sense as a replacement for BEM modifiers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Well, that wasn't quite the answer I expected.&lt;/p&gt;

&lt;p&gt;But it got me thinking. Maybe the more important question isn't which CSS methodology an agent should use, but how to make sure it understands the decisions already made in a project.&lt;/p&gt;

&lt;h2&gt;
  
  
  BEM, modifiers, and data attributes
&lt;/h2&gt;

&lt;p&gt;Here's a typical BEM example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card card--featured"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card__title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Hello World&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card__description"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    A short description.
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like how predictable this is. The relationship between the elements is obvious, and I don't have to worry too much about a generic &lt;code&gt;.title&lt;/code&gt; selector affecting something elsewhere.&lt;/p&gt;

&lt;p&gt;That's useful when working with global CSS.&lt;/p&gt;

&lt;p&gt;Of course, BEM doesn't actually isolate styles. It relies on naming conventions to avoid collisions. And I don't think every simple layout needs a complete BEM structure.&lt;/p&gt;

&lt;p&gt;But what about modifiers?&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button button--primary button--large"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Save
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;data-variant=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;
  &lt;span class="na"&gt;data-size=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Save
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I actually prefer the second version here. The attributes make it clear that &lt;code&gt;variant&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt; are independent properties.&lt;/p&gt;

&lt;p&gt;Modern CSS handles this quite nicely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;&amp;amp;:where([data-variant="primary"])&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;:where&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nt"&gt;data-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"large"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;:where()&lt;/code&gt; prevents the attribute condition from adding specificity. Without it, &lt;code&gt;.button[data-variant="primary"]&lt;/code&gt; would be more specific than &lt;code&gt;.button--primary&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So far, so good.&lt;/p&gt;

&lt;p&gt;But Codex had a point. Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card card--flat"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt; &lt;span class="na"&gt;data-variant=&lt;/span&gt;&lt;span class="s"&gt;"flat"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What exactly do I gain by replacing the first with the second?&lt;/p&gt;

&lt;p&gt;Not much, especially if the existing modifier is already understood throughout the project.&lt;/p&gt;

&lt;p&gt;I still like data attributes for components with multiple named variants. But changing working code just because another syntax looks a little cleaner doesn't seem particularly useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do we need the modifier at all?
&lt;/h2&gt;

&lt;p&gt;The Codex agent also raised a more useful point: if a component already exposes its state through a semantic attribute, why introduce another representation just for styling?&lt;/p&gt;

&lt;p&gt;Imagine an accordion trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion__trigger accordion__trigger--open"&lt;/span&gt;
  &lt;span class="na"&gt;data-state=&lt;/span&gt;&lt;span class="s"&gt;"open"&lt;/span&gt;
  &lt;span class="na"&gt;aria-expanded=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Details
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have three representations of the same state. The BEM modifier says it's open, the data attribute says it's open, and &lt;code&gt;aria-expanded&lt;/code&gt; says it's open.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;For an ordinary disclosure component, I could avoid this entirely by using native HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion__trigger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Details&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Some additional information.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.accordion&lt;/span&gt;&lt;span class="nd"&gt;:where&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.accordion__trigger&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser already manages the open state. And if I need several disclosure elements where only one can be open at a time, the &lt;code&gt;name&lt;/code&gt; attribute on &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; can handle that too.&lt;/p&gt;

&lt;p&gt;Not every accordion or disclosure interface fits the native element, of course. For a custom implementation, &lt;code&gt;aria-expanded&lt;/code&gt; may still be necessary. In that case, I can use it directly for styling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.accordion__trigger&lt;/span&gt;&lt;span class="nd"&gt;:where&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nt"&gt;aria-expanded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Expanded styles */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there's less state to keep synchronized.&lt;/p&gt;

&lt;p&gt;The same applies to disabled buttons. HTML already has &lt;code&gt;disabled&lt;/code&gt;, and CSS provides &lt;code&gt;:disabled&lt;/code&gt;. There's no reason to invent another representation of that state.&lt;/p&gt;

&lt;p&gt;This doesn't mean adding ARIA attributes just for styling. They still need to describe the interface correctly. And &lt;code&gt;data-*&lt;/code&gt; remains useful for custom states that don't have an appropriate native or ARIA representation.&lt;/p&gt;

&lt;p&gt;But before introducing another modifier, it's worth checking whether HTML already provides what we need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would another CSS methodology help?
&lt;/h2&gt;

&lt;p&gt;I also looked beyond BEM and data attributes.&lt;/p&gt;

&lt;p&gt;CSS Modules, Tailwind, CUBE CSS, ITCSS, and other approaches offer different ways to organize or write styles. But for my setup, only a few seem particularly relevant.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS Modules
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/css-modules/css-modules" rel="noopener noreferrer"&gt;CSS Modules&lt;/a&gt; solves one of BEM's problems differently: instead of relying on globally unique class names, it scopes them during the build.&lt;/p&gt;

&lt;p&gt;That's attractive. Classes like &lt;code&gt;.title&lt;/code&gt; or &lt;code&gt;.icon&lt;/code&gt; can be local to a component without requiring a naming convention.&lt;/p&gt;

&lt;p&gt;But I'd also need a transformation step and a way to use the generated class mappings in my Marten templates.&lt;/p&gt;

&lt;p&gt;If I were already using a component-based frontend with a build pipeline, CSS Modules would be an obvious candidate.&lt;/p&gt;

&lt;p&gt;For my current setup, I don't have enough problems with class collisions to justify adding that integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tailwind CSS
&lt;/h3&gt;

&lt;p&gt;I initially associated Tailwind with a much larger frontend toolchain. That wasn't entirely fair.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tailwindcss.com/docs/installation/tailwind-cli" rel="noopener noreferrer"&gt;Tailwind's CLI&lt;/a&gt; can compile static CSS during a build, without requiring React, Vite, or JavaScript in the browser. Adding it to my Containerfile wouldn't necessarily be difficult.&lt;/p&gt;

&lt;p&gt;The bigger difference is how I'd work with CSS.&lt;/p&gt;

&lt;p&gt;Instead of writing component styles separately, I'd compose utilities directly in the markup.&lt;/p&gt;

&lt;p&gt;I can see why that's useful, including for AI agents. Tailwind provides a predefined vocabulary rather than leaving every class name up to the developer or agent.&lt;/p&gt;

&lt;p&gt;But I still prefer meaningful component classes over long utility lists in my templates.&lt;/p&gt;

&lt;p&gt;And Tailwind has conventions of its own, such as making sure its source scanner can detect the utility classes being used.&lt;/p&gt;

&lt;p&gt;I'm not convinced that switching would make my development workflow better. It would mostly make it different.&lt;/p&gt;

&lt;h3&gt;
  
  
  CUBE CSS
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://cube.fyi/" rel="noopener noreferrer"&gt;CUBE CSS&lt;/a&gt; is probably the approach I found most relevant to my existing setup.&lt;/p&gt;

&lt;p&gt;It separates Composition, Utility, Block, and Exception. I particularly like the distinction between layout composition and component styling.&lt;/p&gt;

&lt;p&gt;Unlike CSS Modules or Tailwind, I can borrow these ideas without changing my toolchain.&lt;/p&gt;

&lt;p&gt;I can keep my stylesheets, use BEM-style names where they make sense, and organize the CSS a little more deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  And CSS itself?
&lt;/h2&gt;

&lt;p&gt;Part of the reason I'm comfortable keeping vanilla CSS is how much the language has improved.&lt;/p&gt;

&lt;p&gt;Nesting, custom properties, container queries, cascade layers, and &lt;code&gt;@scope&lt;/code&gt; all address problems that previously required additional conventions or tooling.&lt;/p&gt;

&lt;p&gt;For example, cascade layers let me explicitly define the precedence of different groups of styles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;@scope&lt;/code&gt; lets me target elements within a particular part of the DOM without relying entirely on globally unique selectors.&lt;/p&gt;

&lt;p&gt;It's now supported in current versions of the major browsers, although older browser support still matters. It isn't the same kind of isolation as CSS Modules either.&lt;/p&gt;

&lt;p&gt;None of these features replaces BEM, but they give me more options for solving specific problems without introducing another toolchain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed with AI?
&lt;/h2&gt;

&lt;p&gt;After looking into these approaches, I couldn't find convincing evidence that AI agents produce more maintainable CSS with BEM, Tailwind, CSS Modules, or data attributes.&lt;/p&gt;

&lt;p&gt;Each has plausible advantages.&lt;/p&gt;

&lt;p&gt;Tailwind provides a shared vocabulary. CSS Modules offers local class scoping. BEM makes naming patterns predictable.&lt;/p&gt;

&lt;p&gt;But I don't think we can conclude that one is inherently better for AI-assisted development.&lt;/p&gt;

&lt;p&gt;What matters more to me is consistency.&lt;/p&gt;

&lt;p&gt;If an agent works on a component that already uses BEM, it shouldn't introduce data attributes just because they look more modern.&lt;/p&gt;

&lt;p&gt;If a component already exposes its state through &lt;code&gt;aria-expanded&lt;/code&gt;, it shouldn't add &lt;code&gt;data-state&lt;/code&gt; to represent the same thing.&lt;/p&gt;

&lt;p&gt;And if the project already has design tokens, it shouldn't invent a slightly different color or spacing value for every new component.&lt;/p&gt;

&lt;p&gt;So rather than replacing my CSS architecture, I've been making its conventions more explicit.&lt;/p&gt;

&lt;p&gt;For one of my projects, I put the UI guidance into a repository-local Codex skill under &lt;code&gt;.agents/skills/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The skill describes existing components, where shared design tokens belong, how variants and UI states should be handled, and which stylesheets to look at. It also points to the component gallery, so an agent has actual examples rather than just abstract rules.&lt;/p&gt;

&lt;p&gt;The relevant conventions can be summarized as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Component styling&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Follow existing component and CSS conventions.
&lt;span class="p"&gt;-&lt;/span&gt; Reuse shared design tokens.
&lt;span class="p"&gt;-&lt;/span&gt; Check existing components before creating new ones.
&lt;span class="p"&gt;-&lt;/span&gt; Preserve clear BEM-style names.
&lt;span class="p"&gt;-&lt;/span&gt; Use native HTML states where appropriate.
&lt;span class="p"&gt;-&lt;/span&gt; Reuse semantically correct ARIA states for styling.
&lt;span class="p"&gt;-&lt;/span&gt; Avoid duplicating state across classes and attributes.
&lt;span class="p"&gt;-&lt;/span&gt; Use data attributes when they make variants clearer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the principles rather than a verbatim extract from the skill.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;AGENTS.md&lt;/code&gt;, a short reference is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;For changes to UI templates, CSS, or visual Stimulus states,
use the repository-local UI skill.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this separation. The agent has access to detailed UI guidance when it's relevant, without loading all of it for unrelated backend work.&lt;/p&gt;

&lt;p&gt;It's not a new CSS architecture. It's a way to make the existing one easier to understand and continue.&lt;/p&gt;

&lt;p&gt;Of course, the skill doesn't guarantee correct code. I still have to review what the agent produces.&lt;/p&gt;

&lt;p&gt;But it reduces how much of the project's styling conventions I have to explain repeatedly.&lt;/p&gt;

&lt;p&gt;And perhaps that's where AI has changed my approach the most. Decisions that previously lived in my head now benefit from being documented somewhere an agent can find them.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, is BEM still worth using?
&lt;/h2&gt;

&lt;p&gt;For me, yes.&lt;/p&gt;

&lt;p&gt;I don't see a reason to replace my current CSS setup. It's comfortable, requires very little maintenance, and works well for the applications I'm building.&lt;/p&gt;

&lt;p&gt;I'll continue using BEM where it makes sense, use data attributes when they add clarity, and take advantage of modern CSS features when they solve an actual problem.&lt;/p&gt;

&lt;p&gt;There's also plenty to learn from approaches like CUBE CSS without adopting an entirely new system.&lt;/p&gt;

&lt;p&gt;But the most useful result of this research wasn't discovering a better naming convention.&lt;/p&gt;

&lt;p&gt;It was making the conventions I already use more explicit.&lt;/p&gt;

&lt;p&gt;And I still find it amusing that the Codex agent, which prompted me to question my CSS approach in the first place, ended up arguing against changing it just for the sake of change.&lt;/p&gt;

&lt;p&gt;I think I'll take its advice on this one.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Hotwire Marten: Updating Just the Parts That Changed</title>
      <dc:creator>Marvin Ahlgrimm</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:30:04 +0000</pubDate>
      <link>https://dev.to/treagod/hotwire-marten-updating-just-the-parts-that-changed-281d</link>
      <guid>https://dev.to/treagod/hotwire-marten-updating-just-the-parts-that-changed-281d</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/treagod/hotwire-marten-less-javascript-more-interactivity-3f5i"&gt;the first part&lt;/a&gt;, we wired up Hotwire in a &lt;a href="https://martenframework.com/" rel="noopener noreferrer"&gt;Marten application&lt;/a&gt; and built a small task list. Clicking "Create task" loaded a form into a Turbo Frame. Pretty nice for something that barely involved JavaScript!&lt;/p&gt;

&lt;p&gt;But a task app where you can't complete a task feels a little broken. Nothing is done or is everything already done? Or maybe I just forgot to add some mechanisms for that 🤷🏻.&lt;/p&gt;

&lt;p&gt;Let's fix that with Turbo Streams. New tasks should appear in the list without a full page reload, completing a task should update just that item, and deleting one should remove it.&lt;/p&gt;

&lt;p&gt;Turbo, Stimulus, and importmap are already in place from the first part, so we can focus on the behavior this time. We'll start with task creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render a task once
&lt;/h2&gt;

&lt;p&gt;Before adding the streams, let's extract the markup for a task into &lt;code&gt;src/templates/tasks/_task.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"{% dom_id task %}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;{{ task.title }}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dom_id&lt;/code&gt; tag comes from marten-turbo. For a task with ID 42, it produces &lt;code&gt;task_42&lt;/code&gt;. We'll use that ID later to update or remove exactly this element.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;tasks/list.html&lt;/code&gt;, replace the existing list with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"tasks"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"empty"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;No tasks yet.&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  {% for task in records %}
    {% include "tasks/_task.html" %}
  {% endfor %}
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The surrounding "Create task" link and empty &lt;code&gt;new_task&lt;/code&gt; frame stay as they were. The list now has an ID too, so we can append things to it.&lt;/p&gt;

&lt;p&gt;One small detail: the empty message is always in the HTML. A little CSS handles its visibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#tasks&lt;/span&gt; &lt;span class="nc"&gt;.empty&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:only-child&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a task appears, the message hides. When we delete the last task, it comes back. Conveniently, our stream responses don't have to do anything about it. Modern CSS is awesome!&lt;/p&gt;

&lt;p&gt;Also add &lt;code&gt;ordering :id&lt;/code&gt; to &lt;code&gt;Tasks::ListHandler&lt;/code&gt;. We're going to append new tasks at the bottom, so the initial page should use the same order. Otherwise a reload could rearrange our list. Small but important!&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a task with Turbo Streams
&lt;/h2&gt;

&lt;p&gt;Our create handler already knows how to validate and save a task. We can keep that and switch its parent class to the Turbo-aware version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CreateHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;MartenTurbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Handlers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordCreate&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Task&lt;/span&gt;
  &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="no"&gt;TaskSchema&lt;/span&gt;
  &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="s2"&gt;"tasks/create.html"&lt;/span&gt;
  &lt;span class="n"&gt;turbo_stream_name&lt;/span&gt; &lt;span class="s2"&gt;"tasks/create.turbo_stream.html"&lt;/span&gt;
  &lt;span class="n"&gt;record_context_name&lt;/span&gt; &lt;span class="s2"&gt;"task"&lt;/span&gt;
  &lt;span class="n"&gt;success_route_name&lt;/span&gt; &lt;span class="s2"&gt;"home"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;turbo_stream_name&lt;/code&gt; tells the handler which template to render after a successful Turbo submission. &lt;code&gt;record_context_name&lt;/code&gt; makes the created record available as &lt;code&gt;task&lt;/code&gt;, matching the variable our partial already uses.&lt;/p&gt;

&lt;p&gt;Now create &lt;code&gt;src/templates/tasks/create.turbo_stream.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% turbo_stream 'append' 'tasks' template: 'tasks/_task.html' %}
{% turbo_stream 'replace' 'new_task' do %}&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"new_task"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;{% end_turbo_stream %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire response template.&lt;/p&gt;

&lt;p&gt;After saving, the task appears in the list and the form closes.&lt;/p&gt;

&lt;p&gt;Why replace the frame instead of removing it? Our "Create task" link still needs that frame as its target. Replacing it also clears the &lt;code&gt;src&lt;/code&gt; attribute Turbo added when it loaded the form, so clicking the link again can fetch a fresh form.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about validation?
&lt;/h2&gt;

&lt;p&gt;A successful submission is the easy part. Let's submit an empty title.&lt;/p&gt;

&lt;p&gt;We want the form to stay open and show the error from Marten. For that, move the frame and form into &lt;code&gt;src/templates/tasks/_form.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"new_task"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'tasks_create' %}"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    {% csrf_input %}
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"{{ schema.title.id }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Title:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"{{ schema.title.id }}"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"{{ schema.title.id }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    {% for error in schema.title.errors %}
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input-error"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ error.message }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    {% endfor %}
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Create&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then replace the frame in &lt;code&gt;tasks/create.html&lt;/code&gt; with &lt;code&gt;{% include "tasks/_form.html" %}&lt;/code&gt;. Keep the page's template inheritance and content block around it.&lt;/p&gt;

&lt;p&gt;I've removed the input's &lt;code&gt;required&lt;/code&gt; attribute here so we can actually send an empty title to Marten and see its validation response. The schema still validates the title on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskSchema&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;max_size: &lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;completed&lt;/code&gt; field is gone from this schema. Creating a task only needs a title; completion will get its own schema in a moment.&lt;/p&gt;

&lt;p&gt;Add one more setting to &lt;code&gt;Tasks::CreateHandler&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;turbo_invalid_stream_name&lt;/span&gt; &lt;span class="s2"&gt;"tasks/create_invalid.turbo_stream.html"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And create that template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% turbo_stream 'replace' 'new_task' template: 'tasks/_form.html' %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler returns a 422 response for invalid submissions and renders the form with Marten's validation errors. The form stays open so we can try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finally, completing some work
&lt;/h2&gt;

&lt;p&gt;Let's add a checkbox. First, a small schema in &lt;code&gt;src/schemas/task_completion_schema.cr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskCompletionSchema&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an update handler in &lt;code&gt;src/handlers/tasks/complete_handler.cr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CompleteHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;MartenTurbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Handlers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordUpdate&lt;/span&gt;
  &lt;span class="n"&gt;http_method_names&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;

  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Task&lt;/span&gt;
  &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="no"&gt;TaskCompletionSchema&lt;/span&gt;
  &lt;span class="n"&gt;turbo_stream_name&lt;/span&gt; &lt;span class="s2"&gt;"tasks/complete.turbo_stream.html"&lt;/span&gt;
  &lt;span class="n"&gt;record_context_name&lt;/span&gt; &lt;span class="s2"&gt;"task"&lt;/span&gt;
  &lt;span class="n"&gt;success_route_name&lt;/span&gt; &lt;span class="s2"&gt;"home"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add its route inside &lt;code&gt;Marten.routes.draw&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;"/tasks/&amp;lt;pk:int&amp;gt;/complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CompleteHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"tasks_complete"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler only accepts POST requests. We don't need a separate page for editing one checkbox.&lt;/p&gt;

&lt;p&gt;Its stream template, &lt;code&gt;tasks/complete.turbo_stream.html&lt;/code&gt;, is another one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% turbo_stream 'replace' task template: 'tasks/_task.html' %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time we pass the record itself as the target. The tag derives its DOM ID, so &lt;code&gt;task&lt;/code&gt; becomes something like &lt;code&gt;task_42&lt;/code&gt;, matching the &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; from our partial.&lt;/p&gt;

&lt;p&gt;Now update &lt;code&gt;_task.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"{% dom_id task %}"&lt;/span&gt;&lt;span class="err"&gt;{%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;task.completed&lt;/span&gt; &lt;span class="err"&gt;%}&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"completed"&lt;/span&gt;&lt;span class="err"&gt;{%&lt;/span&gt; &lt;span class="na"&gt;endif&lt;/span&gt; &lt;span class="err"&gt;%}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'tasks_complete' pk: task.pk %}"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    {% csrf_input %}
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"completed"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"completed"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Completed"&lt;/span&gt;&lt;span class="err"&gt;{%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;task.completed&lt;/span&gt; &lt;span class="err"&gt;%}&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="err"&gt;{%&lt;/span&gt; &lt;span class="na"&gt;endif&lt;/span&gt; &lt;span class="err"&gt;%}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;{{ task.title }}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And give completed tasks a little styling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#tasks&lt;/span&gt; &lt;span class="nc"&gt;.completed&lt;/span&gt; &lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#888&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;line-through&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the box and click Save. The task is saved and shown as completed.&lt;/p&gt;

&lt;p&gt;There's a slightly sneaky detail in the form: the hidden input before the checkbox. An unchecked checkbox sends no value at all. We need an explicit &lt;code&gt;0&lt;/code&gt; to clear an existing completion value. When checked, both values are sent, and Marten takes the last one: &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've used Rails form helpers, this probably looks familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  A little Stimulus, finally
&lt;/h2&gt;

&lt;p&gt;The Save button works, but clicking it after every checkbox change feels unnecessary. This is a good job for Stimulus.&lt;/p&gt;

&lt;p&gt;Generate a controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;marten stimulus generate controller autosubmit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace its generated contents with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestSubmit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the controller and action to the completion form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'tasks_complete' pk: task.pk %}"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;
      &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"autosubmit"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"change-&amp;gt;autosubmit#submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checkbox's &lt;code&gt;change&lt;/code&gt; event bubbles up to the form, where Stimulus calls &lt;code&gt;submit()&lt;/code&gt;. We can put the Save button inside &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; so it's only displayed when JavaScript is disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;noscript&amp;gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&amp;lt;/noscript&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our importmap and controller loading were already configured in the first part. Rebuild and restart the server after adding the controller so its pin appears in the generated importmap.&lt;/p&gt;

&lt;p&gt;Now clicking the checkbox submits the form, saves the task, and replaces the list item. Stimulus also connects the controller on the replacement form, so the next click works too.&lt;/p&gt;

&lt;p&gt;And that's all the custom JavaScript for this feature. The controller doesn't even know what a task is. It just submits its form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting a task
&lt;/h2&gt;

&lt;p&gt;We can create and complete tasks now. Let's also add a way to delete them.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;src/handlers/tasks/delete_handler.cr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeleteHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;MartenTurbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Handlers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordDelete&lt;/span&gt;
  &lt;span class="n"&gt;http_method_names&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;

  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Task&lt;/span&gt;
  &lt;span class="n"&gt;success_route_name&lt;/span&gt; &lt;span class="s2"&gt;"home"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;turbo_stream&lt;/span&gt;
    &lt;span class="no"&gt;MartenTurbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TurboStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler also lets us return a &lt;code&gt;MartenTurbo::TurboStream&lt;/code&gt; directly from Crystal. For deletion, that saves us a template file.&lt;/p&gt;

&lt;p&gt;Add the route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;"/tasks/&amp;lt;pk:int&amp;gt;/delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeleteHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"tasks_delete"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add a second form after the title's &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; inside &lt;code&gt;_task.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'tasks_delete' pk: task.pk %}"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {% csrf_input %}
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click Delete, the record is deleted, and the stream removes its list item. If that was the last task, our empty message becomes visible again through CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we ended up
&lt;/h2&gt;

&lt;p&gt;We now have a task list where creating, completing, and deleting tasks updates just the relevant elements on the page.&lt;/p&gt;

&lt;p&gt;What I like about the Marten integration is how little we had to add to our existing handlers. We switched to the Turbo-aware base classes, configured the stream templates, and reused the same task partial throughout. The only custom JavaScript is our small autosubmit controller, but it feels SPA like nonetheless!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>hotwire</category>
      <category>crystal</category>
    </item>
    <item>
      <title>A language for humans and computers: Crystal in the AI era</title>
      <dc:creator>Marvin Ahlgrimm</dc:creator>
      <pubDate>Wed, 07 Oct 2026 15:44:46 +0000</pubDate>
      <link>https://dev.to/treagod/a-language-for-humans-and-computers-crystal-in-the-ai-era-52kj</link>
      <guid>https://dev.to/treagod/a-language-for-humans-and-computers-crystal-in-the-ai-era-52kj</guid>
      <description>&lt;p&gt;At Rails World 2026, DHH described a division of labour that caught my attention: let agents write Rust and enjoy the result without having to look at the code.&lt;/p&gt;

&lt;p&gt;At around 30:15 in the &lt;a href="https://www.youtube.com/watch?v=vDjW_dRyKXY" rel="noopener noreferrer"&gt;opening keynote&lt;/a&gt;, he puts it simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Agents like Rust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I can see the appeal, and I see the same sentiment at my workplace. When an agent writes the code, the effort of writing it by hand matters less. The compiler can catch mistakes as the agent works.&lt;/p&gt;

&lt;p&gt;But I still need and want to understand the software I am responsible for. For me, that points to Crystal.&lt;/p&gt;

&lt;p&gt;I work with Ruby and Crystal and contribute to the &lt;a href="https://martenframework.com/" rel="noopener noreferrer"&gt;Marten web framework&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Familiar syntax, static checks
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://crystal-lang.org/" rel="noopener noreferrer"&gt;Crystal combines Ruby-inspired syntax, static type checking and type inference&lt;/a&gt;, and compiles to native code. If you come from Ruby, much of it will look familiar.&lt;/p&gt;

&lt;p&gt;That familiarity matters even when I am no longer typing every line. I still read the implementation, question its choices and make changes myself. Type inference keeps many annotations out of the way while the compiler checks how values are used.&lt;/p&gt;

&lt;p&gt;Static checks on nullable values are available in other languages, too. What interests me about Crystal is having those checks in code I find straightforward to read. The agent gets feedback from the compiler; I get an implementation I can work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compiler cannot choose the right tools
&lt;/h2&gt;

&lt;p&gt;How do I know the agent chose the right libraries and integration approach?&lt;/p&gt;

&lt;p&gt;Later in the talk, around 32:15, DHH describes assigning work asynchronously and reviewing the result. In my projects, that review includes the choices inside the implementation.&lt;/p&gt;

&lt;p&gt;I recently tried setting up a small Marten application with only one prompt. During implementation, I had to correct the agent twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I wanted Marten Turbo and Marten Stimulus. The agent used npm to install the JavaScript packages instead of the CLI provided by &lt;a href="https://github.com/treagod/marten-importmap" rel="noopener noreferrer"&gt;Marten-importmap&lt;/a&gt;, which was the integration approach I wanted.&lt;/li&gt;
&lt;li&gt;It started developing its own throttling middleware instead of using &lt;a href="https://github.com/treagod/marten-throttle" rel="noopener noreferrer"&gt;Marten-throttle&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I maintain these Marten shards, so I recognised the detours. npm was not the setup I wanted, and the custom middleware would have given me more code to maintain. Neither choice was a type error. Correcting them took knowledge of the project and its libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where compiler feedback has helped me
&lt;/h2&gt;

&lt;p&gt;A few months ago, when the models I used were less capable, I repeatedly saw compilation errors in generated Crystal code. Some involved incorrect handling of &lt;code&gt;String?&lt;/code&gt; values. In some cases, compiler feedback led the agent towards a correction without me having to explain the type problem myself.&lt;/p&gt;

&lt;p&gt;A small example shows what the compiler can catch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first argument might be missing. Its type is &lt;code&gt;String | Nil&lt;/code&gt;, also written &lt;code&gt;String?&lt;/code&gt;. Calling &lt;code&gt;upcase&lt;/code&gt; directly is rejected because &lt;code&gt;Nil&lt;/code&gt; does not provide that method.&lt;/p&gt;

&lt;p&gt;An ordinary conditional handles the missing value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"GUEST"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;if&lt;/code&gt; branch, the compiler knows the local variable cannot be &lt;code&gt;nil&lt;/code&gt;. Crystal documents this &lt;a href="https://crystal-lang.org/reference/1.21/syntax_and_semantics/if_var.html" rel="noopener noreferrer"&gt;type narrowing and its limitations&lt;/a&gt;, including why repeated getter calls cannot always be narrowed in the same way.&lt;/p&gt;

&lt;p&gt;Writing another &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;else&lt;/code&gt; can feel tedious. I appreciate it more when an agent writes the code: I can see what it decided to do when the argument is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling still leaves a decision open
&lt;/h2&gt;

&lt;p&gt;There is another way to get past the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not_nil!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles, but raises an exception when the argument is missing. The &lt;a href="https://crystal-lang.org/api/1.21.0/Object.html" rel="noopener noreferrer"&gt;standard-library documentation&lt;/a&gt; describes that behaviour and recommends avoiding &lt;code&gt;not_nil!&lt;/code&gt; where possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/crystal-ameba/ameba/blob/master/src/ameba/rule/lint/not_nil.cr" rel="noopener noreferrer"&gt;Ameba's &lt;code&gt;Lint/NotNil&lt;/code&gt; rule&lt;/a&gt; flags the assertion in this example. With the rule enabled, the linter gives the agent feedback about a shortcut the compiler accepts.&lt;/p&gt;

&lt;p&gt;The requirement is still open. Should a missing name produce a guest label? Should it stop the operation? Is absence supposed to be impossible? The compiler accepts both the guest fallback and the assertion. I still have to decide which matches the requirement.&lt;/p&gt;

&lt;p&gt;I still need end-to-end checks in both Marten applications and fixes or features for Meridian, my deployment tool. Parts of that work can already be automated, but the agents I use do not reliably cover all my requirements.&lt;/p&gt;

&lt;p&gt;Code and tests generated from the same misunderstanding can agree with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Libraries and compiler feedback
&lt;/h2&gt;

&lt;p&gt;The Marten experiment showed that I cannot assume the agent will find the libraries I want. In a smaller ecosystem, I need to name them and provide current documentation.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://www.microsoft.com/en-us/research/publication/adapting-language-models-for-low-resource-programming-languages/" rel="noopener noreferrer"&gt;study on code generation for low-resource programming languages&lt;/a&gt; explores how tools and feedback can help models work with less familiar languages. I do not know what caused the mistakes in my experiments. I check the agent's library choices and language usage either way.&lt;/p&gt;

&lt;p&gt;Waiting for the compiler also adds up. Long development builds slow down every round of corrections.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2607.13921v2" rel="noopener noreferrer"&gt;Generative Compilation preprint&lt;/a&gt; explores compiler feedback during the generation of partial Rust programs and reports improvements over feedback after complete generation. Its results suggest that the timing of compiler feedback matters as well as the checks themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Rust offers
&lt;/h2&gt;

&lt;p&gt;Rust's &lt;a href="https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html" rel="noopener noreferrer"&gt;ownership model&lt;/a&gt; and &lt;a href="https://doc.rust-lang.org/book/ch16-00-concurrency.html" rel="noopener noreferrer"&gt;concurrency checks&lt;/a&gt; provide guarantees Crystal's garbage collector does not replace. Rust also offers &lt;a href="https://doc.rust-lang.org/cargo/commands/cargo-check.html" rel="noopener noreferrer"&gt;cargo check&lt;/a&gt; and &lt;a href="https://doc.rust-lang.org/clippy/" rel="noopener noreferrer"&gt;Clippy&lt;/a&gt; as feedback tools.&lt;/p&gt;

&lt;p&gt;I have not benchmarked my Crystal workflow against Rust. If you work with Rust every day, you may find its code easier to review.&lt;/p&gt;

&lt;p&gt;Crystal can be hard to read, too. Complex macros can hide what the code does, and its &lt;a href="https://crystal-lang.org/reference/1.21/syntax_and_semantics/unsafe.html" rel="noopener noreferrer"&gt;unsafe operations&lt;/a&gt; need careful review. I have to account for that effort even when an agent handles the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code I still want to understand
&lt;/h2&gt;

&lt;p&gt;I expect agents to do more implementation work. In my projects, I still need and want to judge the result, check requirements and understand enough to make changes.&lt;/p&gt;

&lt;p&gt;With Crystal, static feedback helps the agent correct certain mistakes, and its syntax helps me inspect the decisions that remain.&lt;/p&gt;

&lt;p&gt;Crystal calls itself a language for humans and computers. As computers take on more of the writing, the human side becomes a reason to choose it. I want to delegate implementation and remain capable of understanding and changing the result. Crystal makes that practical for me.&lt;/p&gt;

</description>
      <category>crystal</category>
      <category>ai</category>
      <category>programming</category>
      <category>rust</category>
    </item>
    <item>
      <title>Hotwire Marten: Less JavaScript, More Interactivity</title>
      <dc:creator>Marvin Ahlgrimm</dc:creator>
      <pubDate>Fri, 19 Jun 2026 07:05:29 +0000</pubDate>
      <link>https://dev.to/treagod/hotwire-marten-less-javascript-more-interactivity-3f5i</link>
      <guid>https://dev.to/treagod/hotwire-marten-less-javascript-more-interactivity-3f5i</guid>
      <description>&lt;p&gt;Recently I came across a statement that a developer went back to Ruby on Rails from the Crystal ecosystem. The reason: missing Hotwire support. And honestly, I get it!&lt;/p&gt;

&lt;p&gt;I'm a huge fan of the Hotwire utilities - you can write normal server code without too many frontend dependencies. In fact, Turbo brings SPA feeling out of the box!&lt;/p&gt;

&lt;p&gt;I've been developing apps with &lt;a href="https://martenframework.com/" rel="noopener noreferrer"&gt;Marten&lt;/a&gt; for a while. And all of them have had Hotwire built in. I've built a turbo extension for it quite a while ago, but the experience was never the same as with Rails. So I very much understand the statement to go back to Rails. Everything works out of the box!&lt;/p&gt;

&lt;p&gt;Well, how should anyone know that there's already effort to bring better Hotwire support to the Crystal ecosystem, if nobody writes about it? So here it is! In this post I'll walk you through building a task app with Marten using the full Hotwire stack — Turbo Frames, Turbo Streams, and Stimulus. And beyond what's already available in &lt;a href="https://github.com/treagod/marten-turbo" rel="noopener noreferrer"&gt;marten-turbo&lt;/a&gt;, I'm currently working on &lt;a href="https://github.com/treagod/marten-importmap" rel="noopener noreferrer"&gt;importmap&lt;/a&gt; and &lt;a href="https://github.com/treagod/marten-stimulus" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt; integration support, which is coming along nicely. I want to show you where things stand today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initializing the task app
&lt;/h3&gt;

&lt;p&gt;Let's start by setting up a normal Marten project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;marten new project turbo_tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hotwire integration? None 😩 But don't worry — it's relatively simple to add, and once it's set up, the tooling will feel quite natural.&lt;/p&gt;

&lt;p&gt;Let's add marten-turbo and marten-stimulus. The latter will pull in marten-importmap as a transitive dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;marten_stimulus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;treagod/marten-stimulus&lt;/span&gt;
  &lt;span class="na"&gt;marten_turbo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;treagod/marten-turbo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;shards install&lt;/code&gt; and we're halfway there.&lt;/p&gt;

&lt;p&gt;We need to require the new dependencies and register the Marten apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/project.cr&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"marten_stimulus"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"marten_turbo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/cli.cr&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"marten/cli"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"marten_stimulus/cli"&lt;/span&gt;  &lt;span class="c1"&gt;# also loads marten_importmap/cli&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;installed_apps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="no"&gt;MartenImportmap&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;MartenStimulus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;MartenTurbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just like that, Marten has Hotwire support! It recognizes Turbo tags, provides Turbo helpers, and adds CLI commands for importmap and Stimulus.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Initialize Importmap
&lt;/h3&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;marten importmap init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;config/initializers/importmap.cr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config/initializers/importmap_pins.cr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/assets/application.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Pin Hotwire dependencies
&lt;/h3&gt;

&lt;p&gt;Now pin the required packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;marten importmap pin @hotwired/turbo
marten importmap pin @hotwired/stimulus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Boot Turbo and Stimulus
&lt;/h3&gt;

&lt;p&gt;Update &lt;code&gt;src/assets/application.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/turbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;eagerLoadControllersFrom&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stimulus-loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Stimulus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;eagerLoadControllersFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;controllers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Stimulus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;stimulus-loading&lt;/code&gt; import is pinned automatically by &lt;code&gt;MartenStimulus::App&lt;/code&gt; — no manual setup needed. It reads your importmap at runtime and registers any controllers it finds under the &lt;code&gt;controllers/&lt;/code&gt; prefix.&lt;/p&gt;

&lt;p&gt;You'll also need to tell the importmap where your controllers live. In &lt;code&gt;config/initializers/importmap.cr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;pin&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"application.js"&lt;/span&gt;
    &lt;span class="c1"&gt;# Add this line&lt;/span&gt;
    &lt;span class="n"&gt;pin_all_from&lt;/span&gt; &lt;span class="s2"&gt;"src/assets/controllers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;under: &lt;/span&gt;&lt;span class="s2"&gt;"controllers"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, add &lt;code&gt;{% importmap %}&lt;/code&gt; to the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; section of your base template. This renders the importmap JSON and the necessary preloads.&lt;/p&gt;

&lt;p&gt;With that, the boring part is done. Let's build something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task App
&lt;/h2&gt;

&lt;p&gt;Let's build something really innovative: a Task App. An app where you can create a task and mark it as complete once done. Never seen that before 🤞.&lt;/p&gt;

&lt;p&gt;Well it's small enough to showcase the basics of this setup.&lt;/p&gt;

&lt;p&gt;We'll create a task model, let Marten generate the migrations and create a turbo-augmented CRUD interface.&lt;/p&gt;

&lt;p&gt;The model is kept fairly slim in order to not overload this post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:big_int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;auto: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;max_size: &lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turbo_tasks&lt;span class="nv"&gt;$ &lt;/span&gt;marten genmigrations
Generating migrations &lt;span class="k"&gt;for &lt;/span&gt;app &lt;span class="s1"&gt;'main'&lt;/span&gt;:
  › Creating &lt;span class="o"&gt;[&lt;/span&gt;src/migrations/202604270947501_create_main_task_table.cr]... DONE
      ○ Create main_task table
turbo_tasks&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="c"&gt;# Migrate&lt;/span&gt;
turbo_tasks&lt;span class="nv"&gt;$ &lt;/span&gt;marten migrate
Running migrations:
  › Applying main_202604270947501_create_main_task_table... DONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;src/handlers/tasks/list_handler.cr&lt;/code&gt; and &lt;code&gt;src/templates/tasks/list.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ListHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Handlers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordList&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Task&lt;/span&gt;
  &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="s2"&gt;"tasks/list.html"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extend "base.html" %}

{% block title %}Tasks{% endblock %}

{% block content %}
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Tasks&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    {% for task in records %}
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{{ task.title }}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    {% endfor %}
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some more tweaking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;path "/", Tasks::ListHandler, name: "home"&lt;/code&gt; to your &lt;code&gt;config/routes.cr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;{% importmap %}&lt;/code&gt; right before &lt;code&gt;&amp;lt;/head&amp;gt;&lt;/code&gt; inside &lt;code&gt;src/templates/base.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;spin up the server with &lt;code&gt;marten s&lt;/code&gt;, and navigate to &lt;a href="http://127.0.0.1:8000/" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing fancy yet — just a list of zero tasks. But open your dev-tools and check the network tab: Turbo and Stimulus are already loaded without touching any further JavaScript beyond the initialization 😮‍💨.&lt;/p&gt;

&lt;p&gt;From here on, we'll keep the setup intentionally small so the Turbo behavior stays visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo enters the room
&lt;/h2&gt;

&lt;p&gt;Let's add some basic Turbo functionality to see if it really works. We need a create handler, a schema, and a template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tasks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CreateHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Handlers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordCreate&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Task&lt;/span&gt;
  &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="s2"&gt;"tasks/create.html"&lt;/span&gt;
  &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="no"&gt;TaskSchema&lt;/span&gt;
  &lt;span class="n"&gt;success_route_name&lt;/span&gt; &lt;span class="s2"&gt;"home"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskSchema&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extend "base.html" %}
{% block title %}Create Task{% endblock %}
{% block content %}
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Create Task&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"new_task"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'tasks_create' %}"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      {% csrf_input %}
      &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"{{ schema.title.id }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Title:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"{{ schema.title.id }}"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"{{ schema.title.id }}"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Create&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;path "/tasks/create", Tasks::CreateHandler, name: "tasks_create"&lt;/code&gt; to your &lt;code&gt;config/routes.cr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now update the list template to have a link to the new create page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extend "base.html" %}

{% block title %}Tasks{% endblock %}

{% block content %}
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Tasks&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'tasks_create' %}"&lt;/span&gt; &lt;span class="na"&gt;data-turbo-frame=&lt;/span&gt;&lt;span class="s"&gt;"new_task"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Create task&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"new_task"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    {% for task in records %}
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{{ task.title }}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    {% else %}
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;No tasks found. &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'tasks_create' %}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Create your first one&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    {% endfor %}
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's happening here: the "Create task" link has &lt;code&gt;data-turbo-frame="new_task"&lt;/code&gt;, which tells Turbo to fetch the create page and only extract the matching &lt;code&gt;&amp;lt;turbo-frame id="new_task"&amp;gt;&lt;/code&gt; from the response. It swaps that frame into the empty &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; on the list page — no full page reload, no custom JavaScript. Nothing new to seasoned Hotwire devs, but it's still worth mentioning!&lt;/p&gt;

&lt;p&gt;Click the link and the form just appears inline. Submit it and you're back to the list with your new task. That's the SPA feeling Turbo gives you for free.&lt;/p&gt;

&lt;p&gt;That's enough for the first part. In the next one, I'll go further into Turbo and how to use it to not replace the whole page of the task list when updating a task!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>hotwire</category>
      <category>crystal</category>
    </item>
    <item>
      <title>When AI Moved Into My Editor: Faster… and Weirdly Slower</title>
      <dc:creator>Marvin Ahlgrimm</dc:creator>
      <pubDate>Fri, 03 Oct 2025 11:28:29 +0000</pubDate>
      <link>https://dev.to/treagod/when-ai-moved-into-my-editor-faster-and-weirdly-slower-f89</link>
      <guid>https://dev.to/treagod/when-ai-moved-into-my-editor-faster-and-weirdly-slower-f89</guid>
      <description>&lt;p&gt;The first time I asked an AI to write a small snippet of code, I was flabbergasted. It worked—instantly. It saved me the usual search-and-compare detour, and that alone felt worth paying for.&lt;/p&gt;

&lt;p&gt;This was back then when no Thinking models existed. But it was an immediate argument for a subscription for XYZ AI (no ad here 😬 just think of one of the many options).&lt;/p&gt;

&lt;p&gt;From there, I went into a long phase of copy &amp;amp; paste: throw code at an LLM, ask it to analyze or rewrite, and stitch the result back in. It kind of worked. When reasoning models arrived, my confidence went up: wait a minute, wait three—freshly baked code appears. In &lt;a href="https://rubyonrails.org/" rel="noopener noreferrer"&gt;Rails&lt;/a&gt;, that meant small adjustments and I was done. In &lt;a href="https://martenframework.com/" rel="noopener noreferrer"&gt;Marten&lt;/a&gt;, I had to correct more, which made sense — newer framework, less model knowledge.&lt;/p&gt;

&lt;p&gt;Then the next big thing happened: tools that could search the internet. (What kind of technological progress! The internet wasn't a trend that is going away anytime soon 🤯) I started asking the LLM to read Marten's docs first and then implement feature XYZ. Suddenly, I could use LLMs for Marten, too. Great! "No need for using my brain anymore," right? At least it felt like that — because the handler fixes took less time than before.&lt;/p&gt;

&lt;p&gt;Except I still had to pour context into the model and explain compile errors or edge cases. It became a kind of pair programming where I asked for features and an inexperienced junior picked random things from the internet, threw them at me, and hoped it worked. Sometimes it did. Sometimes it didn't.&lt;/p&gt;

&lt;p&gt;Next wave: MCP and CLI tools like Claude or Codex inside my editor. Now the LLM could read my files and pick up the context I’d been manually narrating. I could spin up the CLI, spell out a feature request, and it worked most of the time. When it didn't, I'd say what didn't work. And then again. And again. Also: I won't give an LLM permission to change my files without acknowledging its work first—so I kept the loop manual. That created a very familiar pattern: &lt;code&gt;write the prompt → wait for results → correct the results → fine-tune&lt;/code&gt;. And yes, I sometimes forgot the CLI was sitting there waiting for my response, dragging on the cycle.&lt;/p&gt;

&lt;p&gt;Recently it clicked that this can slow me down. The waiting, the corrections, the context coaching — it adds up. In Rails, this is fine for smaller features. In Marten, I still hit compile errors or subtle misuses that need careful guidance. And I don't want a bot silently editing my codebase while I lose track of what changed.&lt;/p&gt;

&lt;p&gt;My trust issues mostly come from one experience: a bigger chunk of AI-generated code that "&lt;em&gt;magically&lt;/em&gt;" worked — until I needed a behavior change. That request overstrained the model and it started producing incorrect fixes. Because I hadn't reviewed the initial output deeply, adapting it took longer than if I had either built it myself or reviewed it properly to make it adaptable from the start. That one was on me.&lt;/p&gt;

&lt;p&gt;In my little delusion, I even thought AI could basically ship whole projects for me while I skimmed and sprinkled adjustments. &lt;strong&gt;Boy was I wrong.&lt;/strong&gt; I kicked off a bunch of dear side ideas I didn’t have time for, assuming the model would carry them over the finish line. Instead I ended up with a constellation of half-baked repos — and a clear lesson: AI accelerates momentum you already have; it doesn't create it. It won't do the product thinking, the trade-offs, or the patient grind.&lt;/p&gt;

&lt;p&gt;So I'm stepping on the brake a bit. AI is here to stay and it's impressive, but the "AI will take our jobs" hype is exaggerated. It can absolutely solve things — but it's not delivering the consistent quality of an experienced dev. What it does do well is guide you toward a correct path faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’m doing now (so it helps more than it hurts)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with a requirements doc.&lt;/strong&gt; I write a short spec (goal, constraints, edge cases, acceptance criteria), manually refine it, and treat it as the single source of truth.

&lt;ul&gt;
&lt;li&gt;If I have absolutily no idea what I'm dealing with I'm starting a deep research&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Use AI for planning, code mostly by hand.&lt;/strong&gt; From the spec, I ask the LLM “what’s next / risky / missing,” but I hand code most parts and only offload small snippets or algorithms.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Read the docs—with citations.&lt;/strong&gt; With search enabled, I ask the LLM to analyze the documentation of some library/tool, read the answer and check the sources.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Scope it right.&lt;/strong&gt; AI for scaffolding, glue, migrations, rote transforms; humans for domain logic and novel framework patterns.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Front-load constraints.&lt;/strong&gt; State invariants, interfaces, and edge cases up front to avoid rework.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;One prompt → one cohesive diff.&lt;/strong&gt; Treat AI output like a junior’s PR; keep changes small and reviewable.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Tests first (or alongside).&lt;/strong&gt; Have the model draft intent-revealing tests; they surface mismatches early.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Document the "why."&lt;/strong&gt; Ask for a 5-line design rationale to stash next to the change.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Time-box retries.&lt;/strong&gt; If the LLM had its third faulty try, I take over the tricky bit.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Never skip the review&lt;/strong&gt;—even when it "just works." That’s where hidden debt sneaks in.&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI won’t take over anytime soon, but it will help to achieve our goals. Used well, it augments your knowledge and accelerates the momentum you already have. &lt;/p&gt;

&lt;p&gt;Treat it like Little Helper to Gyro: exceptionally clever, creative, and efficient—but still a helper. Keep your guard up: review the final output, require citations when you rely on docs, and let tests plus a short requirements doc be the gatekeepers. That’s how AI helps more than it hurts.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Embrace the Default: Let HTML and CSS Do the Work</title>
      <dc:creator>Marvin Ahlgrimm</dc:creator>
      <pubDate>Sat, 28 Jun 2025 10:55:42 +0000</pubDate>
      <link>https://dev.to/treagod/embrace-the-default-let-html-and-css-do-the-work-ejp</link>
      <guid>https://dev.to/treagod/embrace-the-default-let-html-and-css-do-the-work-ejp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;We often reach for JavaScript too quickly.&lt;br&gt;
But modern HTML and CSS can already do more than you think&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe you know the situation. You want to provide some intuitive user interface where the user has a nice UI with huge elements, like cards, that are easy to click on. You want to make sure that the user can easily see which element is currently selected. So you reach for JavaScript and add some event listeners to change the style of the element when it is clicked.&lt;/p&gt;

&lt;p&gt;In my case the user had to select which payment type they wanted to use. So I had a list of payment types, each represented by a nice little card. The "cash" option should be selected by default and the user could click on a card to select it. The selected card should have a different background color to nicely indicate which payment type was selected and give the user a visual feedback. Because the cards don’t contribute to the form there has to be a way to tell the form which card was selected.&lt;/p&gt;

&lt;p&gt;Being lazy my first thought was "&lt;em&gt;I need to create another Stimulus Controller to handle the click and fill a hidden input with the selected payment type. Ugh..&lt;/em&gt;".&lt;/p&gt;

&lt;p&gt;But then I thought about it for a second and realized that I could do this with &lt;strong&gt;just HTML and CSS&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Radio Buttons
&lt;/h2&gt;

&lt;p&gt;I don't know why, but radio buttons are not in my standard repertoire of UI elements. I always think of them as something old school, something that is used in the 90s. But they are actually a great way to implement the functionality I needed.&lt;/p&gt;

&lt;p&gt;Radio buttons are a form element that allows the user to select one option from a set. They are perfect for this use case because they inherently support the concept of a "selected" state. When you click on a radio button, it automatically updates the state of the form, and you can style the selected button differently using CSS.&lt;/p&gt;

&lt;p&gt;So I created a list of "hidden" radio buttons and labels that reference them via the &lt;code&gt;for&lt;/code&gt; attribute. The labels are styled as cards, and when the user clicks on a card, the corresponding radio button is selected. The selected radio button can then be styled differently using CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;payment_methods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payment.cash'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="s2"&gt;"cash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="s2"&gt;"bi-cash"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payment.creditCard'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="s2"&gt;"credit_card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="s2"&gt;"bi-credit-card"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payment.online'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="s2"&gt;"payment_pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bi-credit-card"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;payment_methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;radio_button&lt;/span&gt; &lt;span class="ss"&gt;:payment_kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"payment_kind_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"radio-card-input"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"payment_kind_&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
                    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card radio-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;i&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bi &lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* keep it in the DOM for a11y */&lt;/span&gt;
&lt;span class="nc"&gt;.radio-card-input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.radio-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--grey-200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--radius&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--white&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-color&lt;/span&gt; &lt;span class="m"&gt;.2s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;box-shadow&lt;/span&gt; &lt;span class="m"&gt;.2s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.radio-card-input&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;.radio-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-rgb&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;.35&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.radio-card-input&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;.radio-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.radio-card&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;.06&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* helper for nice icon sizing */&lt;/span&gt;
&lt;span class="nc"&gt;.radio-card&lt;/span&gt; &lt;span class="nt"&gt;i&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the user to interact with the payment options in a very intuitive way and keep it accessible. The selected payment type is automatically stored in the form, and you can easily access it when the form is submitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But there is more!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Toggle Visibility with CSS
&lt;/h2&gt;

&lt;p&gt;This is probably something you have seen before, but I want to show it again because it is so useful. You can use the &lt;code&gt;:checked&lt;/code&gt; pseudo-class to toggle the visibility of elements based on the state of a radio button.&lt;/p&gt;

&lt;p&gt;In my case I wanted to show a invoice form when the user selects the online payment option. I could have used JavaScript to show and hide the form, but instead I used CSS to toggle the visibility based on the state of the radio button.&lt;/p&gt;

&lt;p&gt;Right underneath the radio button container I put another container &lt;code&gt;&amp;lt;div class="invoice"&amp;gt;&lt;/code&gt;. By default this container is hidden:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.row&lt;/span&gt; &lt;span class="nc"&gt;.invoice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I use the &lt;code&gt;:checked&lt;/code&gt; pseudo-class to show the container when the online payment option is selected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.row&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;#payment_kind_payment_pending&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.invoice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How does that work?&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;&lt;code&gt;:has()&lt;/code&gt; is the long-awaited &lt;strong&gt;parent selector&lt;/strong&gt;: it lets you style a parent (&lt;em&gt;here &lt;code&gt;.row&lt;/code&gt;&lt;/em&gt;) if any child inside it matches a condition – in this case, the radio button with ID &lt;code&gt;payment_kind_payment_pending&lt;/code&gt; is &lt;code&gt;:checked&lt;/code&gt;.  After decades of “CSS can’t look &lt;em&gt;upwards&lt;/em&gt;,” &lt;code&gt;:has()&lt;/code&gt; finally makes it possible 🤯.  It’s a relational pseudo-class which ships in every evergreen browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser support &amp;amp; fallback
&lt;/h3&gt;

&lt;p&gt;As of mid-2025, &lt;code&gt;:has()&lt;/code&gt; enjoys &lt;strong&gt;~93 % global support&lt;/strong&gt; (Chrome 105+, Firefox 117+, Safari 15.4+, Edge 105+).&lt;/p&gt;

&lt;p&gt;For the minority still on older engines, the invoice panel simply stays hidden. If you need to support these too JavaScript will probably solve this.&lt;/p&gt;

&lt;p&gt;This way the invoice form is only visible when the user selects the online payment option. No JavaScript needed! And it’s accessible out of the box too!&lt;/p&gt;

</description>
      <category>html</category>
      <category>css</category>
      <category>a11y</category>
      <category>progressiveenhancement</category>
    </item>
    <item>
      <title>Applying the Presenter Pattern in Marten</title>
      <dc:creator>Marvin Ahlgrimm</dc:creator>
      <pubDate>Mon, 23 Jun 2025 14:54:22 +0000</pubDate>
      <link>https://dev.to/treagod/applying-the-presenter-pattern-in-marten-37cf</link>
      <guid>https://dev.to/treagod/applying-the-presenter-pattern-in-marten-37cf</guid>
      <description>&lt;p&gt;I recently ran into a problem in one of my &lt;a href="https://martenframework.com/" rel="noopener noreferrer"&gt;Marten&lt;/a&gt; projects. My &lt;code&gt;Journey&lt;/code&gt; model — originally quite simple — ended up bloated with methods like &lt;code&gt;travel_period&lt;/code&gt;, &lt;code&gt;total_costs&lt;/code&gt;, sidebar data prep and more. Nearly every view needed these calculations, so my model was being polluted with logic that didn’t belong there. &lt;/p&gt;

&lt;p&gt;Marten templates doesn't allow complex calculations, so I had to do these calculations in my models and access it in my template. But this made the model huge and hard to maintain. It felt wrong to mix UI logic with data persistence.&lt;/p&gt;

&lt;p&gt;I wanted to keep my models clean and focused on their primary role: representing data in the database. But I also needed a way to prepare data for my templates without cluttering the model with view-specific logic.&lt;/p&gt;

&lt;p&gt;To solve this, I borrowed a pattern from Rails and other ecosystems: the &lt;strong&gt;Presenter Pattern&lt;/strong&gt;. Instead of stuffing the &lt;code&gt;Journey&lt;/code&gt; model, I moved view-specific logic into a &lt;code&gt;JourneyPresenter&lt;/code&gt; class. I then plugged that into my handlers so my templates could work cleanly with journey.&lt;/p&gt;

&lt;p&gt;Here’s how I set it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting a Presenter for my &lt;code&gt;Journey&lt;/code&gt; model
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;Journey&lt;/code&gt; model acts as a central wrapper — holding transports, accommodations, travelers, days, and so on. All my journey views show a sidebar with accumulated price, total traveler count, and other derived data. That made it a perfect candidate for moving presentation logic out of the model.&lt;/p&gt;

&lt;p&gt;Initially I thought to use &lt;code&gt;Object#delegate&lt;/code&gt;, but manually listing every method felt brittle.&lt;/p&gt;

&lt;p&gt;Thankfully Crystal’s macro system allowed me to generate delegations compile-time. The result: a reusable &lt;code&gt;Delegate&lt;/code&gt; module that forwards all public, zero-argument methods from the model and mixes in &lt;code&gt;Marten::Template::Object::Auto&lt;/code&gt;, making them accessible in Marten templates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MartenPresenters::Delegate&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Template&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Auto&lt;/span&gt;

  &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nf"&gt;present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="n"&gt;ivar_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"::"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;underscore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;

    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;=&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:initialize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;ivar_name&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;}});&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;

    &lt;span class="kp"&gt;getter&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;ivar_name&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;

    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;methods&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
      &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visibility&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:public&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
        &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;=&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;ivar_name&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This modules macro &lt;code&gt;present&lt;/code&gt; takes a class, which is used to create a initializer that takes an instance of the class we want to present. It also creates delegation functions for all public methods that take no arguments (in assumption they are getters).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sidenote&lt;/strong&gt;: This could be extended to use a blacklist of methods to exclude.&lt;/p&gt;

&lt;p&gt;The macro generates methods that delegate to the wrapped model, so I only need to call &lt;code&gt;start_date!&lt;/code&gt; instead of &lt;code&gt;journey.start_date!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The present &lt;code&gt;Journey&lt;/code&gt; call wraps up model attributes neatly and compile-time delegation avoids manual boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining &lt;code&gt;JourneyPresenter&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JourneyPresenter&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;MartenPresenters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Delegate&lt;/span&gt;

  &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nf"&gt;finished&lt;/span&gt;
    &lt;span class="n"&gt;present&lt;/span&gt; &lt;span class="no"&gt;Journey&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;end_time_formatted&lt;/span&gt;
    &lt;span class="n"&gt;end_date!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%Y-%m-%d"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;total_costs&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not_nil!&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;symbol_first: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sorted_days&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;total_costs_per_traveler&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not_nil!&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;travelers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;symbol_first: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;travel_period&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_date!&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_date!&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important note&lt;/strong&gt;: I wrap &lt;code&gt;present Journey&lt;/code&gt; inside a &lt;code&gt;macro finished&lt;/code&gt; block. Without deferring it, the compiler wouldn’t have fully resolved &lt;code&gt;Journey&lt;/code&gt;’s methods yet — so delegation wouldn’t include them all.&lt;/p&gt;

&lt;p&gt;All &lt;code&gt;Journey&lt;/code&gt; fields are directly accessible, e.g. &lt;code&gt;#start_date!&lt;/code&gt; or &lt;code&gt;#end_date!&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;This presenter now handles formatting dates, accumulates prices and travelers, and provides ordered collections. Models stay lean, and templates stay expressive and focused.&lt;/p&gt;

&lt;h3&gt;
  
  
  Injecting the Journey Presenter into Handlers
&lt;/h3&gt;

&lt;p&gt;I wanted a quick way to make journey available in my templates, so I built this mix-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;HasJourneyContext&lt;/span&gt;
  &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nf"&gt;included&lt;/span&gt;
    &lt;span class="n"&gt;before_render&lt;/span&gt; &lt;span class="ss"&gt;:set_journey_context&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="vi"&gt;@journey&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Journey&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_journey_context&lt;/span&gt;
    &lt;span class="c1"&gt;# Inject the presenter into the templates context&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;journey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JourneyPresenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;journey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;journey&lt;/span&gt;
    &lt;span class="vi"&gt;@journey&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;Journey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;access_token: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordNotFound&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Journey not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using the Presenter in a Real Handler
&lt;/h2&gt;

&lt;p&gt;Here’s an example handler that includes the mix-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TravelerListHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Marten&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Handlers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Template&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;HasJourneyContext&lt;/span&gt; &lt;span class="c1"&gt;# Include for simple usage of the presenter&lt;/span&gt;

  &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="s2"&gt;"travelers/list.html"&lt;/span&gt;

  &lt;span class="n"&gt;before_render&lt;/span&gt; &lt;span class="ss"&gt;:set_travelers&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_travelers&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:travelers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Traveler&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;journey: &lt;/span&gt;&lt;span class="n"&gt;journey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"-created_at"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By including &lt;code&gt;HasJourneyContext&lt;/code&gt;, this handler automatically injects a fully prepared &lt;code&gt;JourneyPresenter&lt;/code&gt; into the templates context.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Achieves
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Models stay focused on persistence — no mix of UI logic&lt;/li&gt;
&lt;li&gt;Templates call &lt;code&gt;{{ journey.travel_period }}&lt;/code&gt; or &lt;code&gt;{{ journey.total_costs }}&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;Presenter class is testable in isolation&lt;/li&gt;
&lt;li&gt;Handlers are lean and declarative&lt;/li&gt;
&lt;li&gt;Macros generate safe, compile-time delegations&lt;/li&gt;
&lt;li&gt;No global state, no hidden context—everything is explicit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything just works, and it’s easy to extend or test.&lt;/p&gt;

</description>
      <category>marten</category>
      <category>crystal</category>
      <category>webdev</category>
      <category>refactorit</category>
    </item>
  </channel>
</rss>
