<?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.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>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>
