Bridgetown2024-03-26T17:28:23-05:00/feed.xmlFlagrantDelightful Reveal #502024-03-25T14:40:06-05:002024-03-25T14:40:06-05:00repo://posts.collection/_posts/delightful-reveal-50-2024-03-25.md<p>“Those developers…what’re you up to?”</p>
<h2 id="what-we-did">What we did:</h2>
<ul>
<li>Designed and built the splash page for RubyConf 2024 for release next week.</li>
<li>Played with kittens.</li>
<li>Began redesigning Training Tracker presentation slides.</li>
<li>Upgraded to Turbo 8 and learned about some cool features it contains.</li>
<li>Created an inline edit form for mobile table rows and then made the rows clickable to open the form with Turbo.</li>
<li>Began working on signage for RubyConf Detroit.</li>
<li>Planned for and led research (road test) sessions with four prospective customers and users. Sent thank you emails.</li>
<li>Analyzed stakeholder interviews.</li>
<li>Solicited feedback from early adopters.</li>
<li>Filed taxes in two countries.</li>
<li>Call for speaker proposals for <a href="https://www.madisonruby.com/">Madison+ Ruby</a> is officially open! What inspires you to be better at what you do? How do you use Ruby in fascinating ways? Don’t miss the April 30th deadline – submit your talk now!</li>
<li>Cleaned up an association in a client app and made it required instead of optional.</li>
</ul>
<h2 id="what-were-reading">What we’re reading:</h2>
<ul>
<li><a href="https://www.goodreads.com/book/show/68495.Iron_Council">Iron Council</a> - China Mievelle </li>
<li><a href="https://www.goodreads.com/book/show/827.The_Diamond_Age">The Diamond Age</a> — Neal Stephenson (again)</li>
<li><a href="https://www.goodreads.com/book/show/59251290-the-bone-shard-war">The Bone Shard War</a> - Andrea Stewart</li>
<li><a href="https://app.thestorygraph.com/books/1f115558-5099-436c-ae54-ec5194ccbe12">A Living Remedy</a> - Nicole Chung</li>
<li>Still reading <a href="https://www.oreilly.com/library/view/developing-apps-with/9781098152475/">Developing Apps with ChatGPT and ChatGPT4</a> possibly for weeks.</li>
<li><a href="https://www.rossgay.net/inciting-joy">Inciting Joy</a> - Ross Gay</li>
<li><a href="https://www.theverge.com/2024/3/21/24105363/apple-doj-monopoly-lawsuit">US sues Apple for illegal monopoly over smartphones - The Verge</a> </li>
<li><a href="http://henryjenkins.org/blog/2024/3/18/girl-crush-k-pop-idols-part-i">“Girl Crush” K-pop Idols: A Conversation between Korean, Chinese, and US Aca-fans: Part I</a> </li>
<li><a href="https://www.goodreads.com/en/book/show/19161852">The Fifth Season</a> - N. K. Jemisin</li>
</ul>
<h2 id="what-were-watching">What we’re watching:</h2>
<ul>
<li>The New Look (<a href="https://tv.apple.com/us/show/the-new-look/umc.cmc.6m0i5dcn60uzl206qq6ibomeh">Apple+</a>)</li>
<li>Slow Horses S1 (<a href="https://tv.apple.com/us/show/slow-horses/umc.cmc.2szz3fdt71tl1ulnbp8utgq5o">Apple+</a>)</li>
<li>Mr. Robot (<a href="https://www.rottentomatoes.com/tv/mr_robot">Rotten Tomatoes</a>)</li>
<li>Shogun (<a href="https://www.rottentomatoes.com/tv/shogun_2024">Rotten Tomatoes</a>)</li>
<li>Tokyo Vice (<a href="https://www.rottentomatoes.com/tv/tokyo_vice">Rotten Tomatoes</a>)</li>
<li>Dragon Ball (<a href="https://www.imdb.com/title/tt0280249/?ref_=nv_sr_srsg_6_tt_8_nm_0_q_*%2520Dragon%2520Ball">IMDb</a>)</li>
<li>Barbie (<a href="https://www.imdb.com/title/tt1517268/">IMDb</a>)</li>
<li>From Hot Metal to HTML: The Story of Typography (<a href="https://www.youtube.com/watch?v=qbCniw-BcW0">YouTube</a>), a talk from Dylan Beattie</li>
<li>The Fifth Element (<a href="https://www.imdb.com/title/tt0119116/">IMDb</a>) – Yes, again.</li>
<li>Pokémon Horizons (<a href="https://www.imdb.com/title/tt26692417/">IMDb</a>)</li>
</ul>
<h2 id="what-were-listening-to">What we’re listening to:</h2>
<ul>
<li><a href="https://www.youtube.com/@CinemaTherapyShow">Cinema Therapy</a></li>
<li><a href="https://open.spotify.com/album/6rjpYHZwFktbc0RCiTfEG6?si=WoJwVBkpQWOvmsNDlXJ7Ag">Unheard</a> - album by Hozier</li>
<li><a href="https://open.spotify.com/track/7dH0dpi751EoguDDg3xx6J?si=0128c9be54e64585">Dried Flower</a> by Yuuri</li>
<li><a href="https://open.spotify.com/track/6je5cTal6PyeITNrOzkCoS?si=45291f7640a24148">Welcome to the Show</a> by Day6</li>
<li><a href="https://open.spotify.com/track/366HAwk1KLvWOwf1tml3jA?si=38609e27ac3f43ad">You Get Me</a> by Twice</li>
<li><a href="https://m.youtube.com/watch?v=KV1EEosiomc">the Black seminole.</a> - Lil Yachty</li>
<li><a href="https://www.youtube.com/watch?v=YVXBTsfp3wQ">Underground Sound</a> - Joey Valence & Brae </li>
</ul>
<h2 id="what-were-interested-in">What we’re interested in:</h2>
<ul>
<li>More interesting capital-P Patterns, like <a href="https://martinfowler.com/bliki/ContradictoryObservations.html">Contradictory Observations</a></li>
<li>What’s the strongest sorcerer subclass, Aberrant Mind or Clockwork Soul</li>
<li>Baldur’s Gate 3</li>
<li><a href="https://www.deathbyfilms.com/latest-artcles/top-90-greatest-ever-polish-movie-posters/">Polish movie posters</a></li>
<li>Seeing the new Ghostbusters: Frozen Empire movie</li>
<li>Cats & Soup (<a href="https://play.google.com/store/apps/details?id=com.hidea.cat&hl=en_US&gl=US&pli=1">mobile game</a>)</li>
<li>MS Flight Simulator</li>
</ul>
<h2 id="what-were-struggling-with">What we’re struggling with:</h2>
<ul>
<li>Plex Meta Manager and <a href="https://metamanager.wiki/en/latest/defaults/overlays/">overlays</a></li>
<li>Competing actions on an input field</li>
<li>Sleep +1</li>
<li>Broken laptop screen</li>
<li>Grief and rest</li>
<li>Improving mile times running</li>
</ul>
<h2 id="what-were-buying">What we’re buying:</h2>
<ul>
<li><a href="https://isotunes.com/products/isotunes-free-replacement-charging-case">A replacement case</a> for my ISOTunes earbuds, because the current one has been dropped one too many times</li>
<li>Casio Classic wristwatch</li>
<li>ETFs</li>
<li>A memecoin</li>
<li>External monitor and new Macbook Pro</li>
<li>Notebooks made using delicious and fountain pen-friendly Tomoe River paper</li>
<li>A metric set of Forstner bits</li>
</ul>
<h2 id="what-were-celebrating">What we’re celebrating:</h2>
<ul>
<li>50th Delightful Reveal</li>
<li>Public domain images of <a href="https://publicdomainreview.org/collection/kittens-and-cats-a-first-reader-1911-cats-and-captions-before-the-internet-age/">old-time cat memes</a></li>
<li>Prepping for an art show!</li>
<li>Finding a buyer for a smooth housing transition</li>
<li>Spring Equinox</li>
<li>Going to see Brett Goldstein in Milwaukee</li>
<li>Spring fly fishing in Northern California</li>
<li>Kittens!</li>
</ul>
<p><img src="https://lh7-us.googleusercontent.com/g-7UTNVdBhbJcZabFRHoOHE13TXMWamYryuvJleNQMrJ-nSCgUkzq3GbQh169G3Bv1wIxdiI-5hFe24DDPC-eujGUgF0UjHmHe1XHBDYRpBLvh4bIBoQ3Qs6pIZdyLn48iSPc0c4iDXjJigVjZAAhh4" alt="" /></p>Jack PermenterFile Access (and a Pattern)2024-03-18T19:20:28-05:002024-03-18T19:20:28-05:00repo://posts.collection/_posts/file-access-and-a-pattern-2024-02-08.md<p>I was working on an import process, basically slurping in a bunch of data and shoving it into a database. Well, a little more nicely than just “shoving”. There’s massaging the data a bit from the input format, validating it, and then going through the normal creation flow (at least the behind-the-scenes part). At heart, this import process is a way of allowing someone to not have to go through a form hundreds of times to get those hundreds of records created.</p>
<p>I’m not here to talk about the import itself, though that could be a nice topic for later. What I’m here to talk about is providing the data. The data is meant to come in a CSV format, and this was a first-pass effort on the importer. That meant it was easy enough to create a CSV file and give the importer the filename. Locally, this worked perfectly.</p>
<p>Then things got a little tricky when I wanted to verify this in the QA environment, and was bluntly reminded of something very important when it comes to Heroku dynos: they’re meant to be stateless. There’s no way to put files there. Even if you shell in, there are no editors installed. I spent a little bit of time seeing how people have tried to get around this, but really, that’s not what you’re meant to do. (Also, and maybe more to the point, none of the things I tried worked.)</p>
<p>This, by the way, is a good reminder that your code will need to work in all environments, not just on your machine. Decades in software development, and I’m still occasionally rudely caught up by this — especially when I’m trying to do something “quick and dirty” or “just as a first pass”.</p>
<p>So, Heroku dynos are meant to be stateless. You’re not meant to add or change files on the dyno itself, but get information and data from other places; a database, environment variables, S3.</p>
<p>Hey, S3. That reminded me — this app already had something for dealing with attachments and knowing what to do with the underlying files. In local dev it just used the local filesystem, but in deployment environments it used S3. Maybe I can use that?</p>
<p>In fact, I couldn’t easily use it as-is, because it was meant to back attachments and deal with “file fields” in forms and interface with the DB nicely. I looked around for other packages, but they all seemed to follow the same pattern of backing attachments, acting as some sort of database model. What I could do was make a simplified copy of the existing attachment-backing code. And in the end, all I had to do was provide the correct configuration for local dev vs. other environments. Oh, and go through the importer code to find calls like <code class="highlighter-rouge">File.stream</code> and <code class="highlighter-rouge">File.write</code> and change the <code class="highlighter-rouge">File</code> to <code class="highlighter-rouge">FileAccess</code>. Simple as that.</p>
<p>That’s one story, but there are many like it. There’s a general pattern here with this common interface implementing different behaviors depending on some configuration — welcome to <a href="https://en.wikipedia.org/wiki/Adapter_pattern">the Adapter pattern</a>.</p>
<p>You remember <a href="https://www.beflagrant.com/blog/design-patterns">Design Patterns</a>, right? When you look around, you can see these patterns all around you. That’s the point. As mentioned before, these are the “building blocks” of creation. But not all building blocks are the same size — some are bricks or boards, some are walls or rooms.</p>
<p>Go forth and look around. And enjoy the patterns.</p>Yossef MendelssohnDelightful Reveal #492024-03-11T11:31:34-05:002024-03-11T11:31:34-05:00repo://posts.collection/_posts/delightful-reveal-49-2024-03-11.md<p>“No apparent blockers other than the concept of time.”</p>
<h2 id="what-we-did">What we did:</h2>
<ul>
<li>Work! +1</li>
<li>Traveled to Miami for <a href="https://www.pbexpo.org/">PartsBase Expo 2024</a> in support of Training Tracker.</li>
<li>Redesigned a comments pill so that people can discover it more easily based on client feedback.</li>
<li>Got feedback from an early adopter on the challenges they’re facing with the product and turned around a few quick changes.</li>
<li>Met with a stakeholder to give them a preview of some usability tests we’re planning to do with our users.</li>
<li>Planned a round of usability tests with prospective customers of our new product.</li>
<li>Found an amazingly bad interaction between our special select component (VirtualSelect) and being in a modal.</li>
<li>Started eliminating all tooltips from a client project.</li>
<li>Designed the UI for admin users to upload logos.</li>
<li>Demoed designs and reviewed upcoming milestones and held internal team meetings to align on project priorities.</li>
<li>Figured out the right way to deal with keypress events to customize a text field so it formats currency and then realized the right way was to use the input event instead.</li>
<li>Disabled all inputs in a form by adding a custom option to the form builder that propagates to all child inputs.</li>
<li>Developed draft roadmaps for planning.</li>
<li>Published an article on <a href="https://www.devopsdigest.com/4-tips-for-maximizing-teams-in-small-software-development-shops">DevOps Digest</a>: 4 Tips for Maximizing Teams in Small Software Development Shops by Jim.</li>
</ul>
<h2 id="what-were-reading">What we’re reading:</h2>
<ul>
<li><a href="https://app.thestorygraph.com/books/6a8fe1fa-9c5c-4461-99cd-8718342f1179">A Thousand Splendid Suns</a> by Khaled Hosseini</li>
<li><a href="https://modalzmodalzmodalz.com/">Modalz Modalz Modalz</a></li>
<li><a href="https://open.spotify.com/show/47pj8aLCCRTyh0OBvGewtk?si=1639dd12db354aa1">The Arsonist’s City</a> by Hala Alyan</li>
<li><a href="https://www.goodreads.com/book/show/32758901-all-systems-red">All Systems Red (The Murderbot Diaries #1)</a> by Martha Wells</li>
<li><a href="https://www.penguinrandomhouse.com/books/626187/portable-magic-by-emma-smith/">Portable Magic: A History Of Books And Their Readers</a> By Emma Smith (just finished!)</li>
<li><a href="https://heydonworks.com/article/your-tooltips-are-bogus/">Your Tooltips are Bogus</a></li>
<li><a href="https://www.oreilly.com/library/view/developing-apps-with/9781098152475/">Developing Apps with GPT-4 and Chat GPT</a> <a href="https://www.oreilly.com/search?q=author:%22Olivier%20Caelen%22">Olivier Caelen</a>, <a href="https://www.oreilly.com/search?q=author:%22Marie-Alice%20Blete%22">Marie-Alice Blete</a></li>
<li><a href="https://open.spotify.com/show/5lIS5YQRT0ipgZt6CgGmx1?si=c6af18b42e1a48ba">Tom Lake</a> by Anne Patchett</li>
</ul>
<h2 id="what-were-watching">What we’re watching:</h2>
<ul>
<li><a href="https://www.imdb.com/title/tt0046250/">Roman Holiday</a></li>
<li><a href="https://www.dunemovie.com/home/">DUNC 1 & DUNC 2</a> (+1 on DUNC 1)</li>
<li><a href="https://gem.cbc.ca/the-great-canadian-pottery-throw-down">The Great Canadian Pottery Throwdown</a> (S1)</li>
<li><a href="https://www.netflix.com/title/80237957">Avatar The Last Airbender</a> (2024)</li>
<li><a href="https://www.bbc.co.uk/programmes/b006mw1h">Gardeners’ World</a></li>
<li><a href="https://www.concacaf.com/w-gold-cup/">CONCACAF Women’s Gold Cup</a></li>
<li><a href="https://www.destroyallsoftware.com/talks/wat">Wat</a></li>
<li><a href="https://www.whitehouse.gov/state-of-the-union-2024/">SOTU</a></li>
<li><a href="https://www.max.com/shows/tokyo-vice/e7d93204-7f98-4e62-ab52-6c1da053f942">Tokyo Vice</a> (S2)</li>
<li><a href="https://en.wikipedia.org/wiki/Sh%C5%8Dgun_(2024_miniseries)">Shogun</a> (2024)</li>
</ul>
<h2 id="what-were-listening-to">What we’re listening to:</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=zFlZUi9TUvY">Wasia Project - impossible (Live Session)</a></li>
<li><a href="https://youtu.be/jcYGfh9PjIU?si=kTDsKr4MKf9a0MnL">Navy Blue</a> by Wooseok</li>
<li><a href="https://youtu.be/kHW-UVXOcLU?si=liAlin9Y1D0XvuAl">Shopper</a> by IU ft. DPR Ian</li>
<li><a href="https://open.spotify.com/track/5504ozM505NGOFiLPSvijC?si=6fccb0f916cd4683">lavender blossom</a> by Salty Licorice</li>
<li><a href="https://open.spotify.com/track/5xo8RrjJ9CVNrtRg2S3B1R?si=1c48117f08b04734https://open.spotify.com/track/5xo8RrjJ9CVNrtRg2S3B1R?si=1c48117f08b04734">Motion Sickness</a> by Pheobe Bridgers</li>
<li><a href="https://www.audible.com/pd/The-Covenant-of-Water-Audiobook/B0BVDVR6XT?source_code=GO1GB547041122911G&ds_rl=1261256&gclid=CjwKCAiAi6uvBhADEiwAWiyRdtUJgFBe-hNTVzEjwXTeYYrF3rMD3RYSlWF7CxdCNk-uieEVSWqLkRoCu4YQAvD_BwE&gclsrc=aw.ds">The Covenant of Water</a> written and read by Abraham Verghese</li>
</ul>
<h2 id="what-were-interested-in">What we’re interested in:</h2>
<ul>
<li><a href="https://help.figma.com/hc/en-us/articles/21635177948567-Edit-objects-on-the-canvas-in-bulk">Figma multi-edit</a></li>
<li>Chipmunks</li>
<li><a href="https://en.wikipedia.org/wiki/UTM_parameters">Urchin Tracking Module</a> (UTM) Parameters that you see on query strings</li>
<li><a href="https://www.slideshare.net/pgconf/not-just-unique-exclusion-constraints">Exclusion constraints in Postgres</a></li>
<li><a href="https://www.themicropedia.org/">The Micropedia of Microaggressions</a> – the first encyclopedia of microaggressions.</li>
</ul>
<h2 id="what-were-struggling-with">What we’re struggling with:</h2>
<ul>
<li>Body</li>
<li>Every day is Monday</li>
<li>Landscaping</li>
</ul>
<h2 id="what-were-buying">What we’re buying:</h2>
<ul>
<li>Silicone molds of tiny pizzas for epoxy resin casting</li>
<li>iPad charger</li>
<li>Rechargeable hand-warmer battery packs</li>
<li>Nespresso pods because coffee</li>
<li>Dune Table Top RPG (TTRPG)</li>
<li>Cheap iPhone SE</li>
</ul>
<h2 id="what-were-celebrating">What we’re celebrating:</h2>
<ul>
<li>Real users using real products that are out there in the world.</li>
<li>Getting quality time with my parents.</li>
<li>Tickets to a 90s cover band night.</li>
<li>Spring is coming!!</li>
<li>Corgi butts</li>
</ul>
<p><img src="https://lh7-us.googleusercontent.com/EOUpuAnvVd1HtJ5P-1mkSRp4ZWjEp7H5sZeY-z9zte-qB10i47Vi0WThgc34FzMsAC5oP_2XN8Ok51LDbBLYJXv9AlyaKlgQhV3DYPiztJuyX9qNEQYNVv0eQPQ5eJB1J_IDP_Wr--zQ1ayaV5NSFCw" alt="" /></p>Jack PermenterHotwire Your Buttons2024-03-07T16:07:56-06:002024-03-07T16:07:56-06:00repo://posts.collection/_posts/hotwire-your-buttons-2024-02-27.md<p>At some point everyone bumps into the age old problem of unintentional multiple form submissions usually caused by repeated pressing of submit buttons. Fortunately, if you are using <a href="https://turbo.hotwired.dev">Turbo</a> you get the automatic <a href="https://turbo.hotwired.dev/handbook/drive#form-submissions">disabling of buttons</a> for free right out of the box.</p>
<p>While Turbo is great at providing these sort of universally helpful features, what makes the <a href="https://hotwired.dev/">Hotwire</a> framework so powerful are the ways that you can easily add more functionality using Turbo <a href="https://turbo.hotwired.dev/reference/attributes">attributes</a> and <a href="https://turbo.hotwired.dev/reference/events">event listeners</a> along with <a href="https://stimulus.hotwired.dev/">Stimulus</a> controllers to plug up any holes that might be missing from the default functionality. In this blog post I will share some ways in which we ‘Hotwired’ the disabling and enabling of buttons in one of our <a href="https://rubyonrails.org/">Rails</a> apps with a little help from <a href="https://tailwindcss.com/">TailwindCss</a> and <a href="https://viewcomponent.org/">ViewComponents</a>.</p>
<p>The first limitation that we ran into with Turbo disabling of buttons is that it re-enables the button right before redirecting after a form submission which still gives time for a user to click the submit button again and can be a bit of a confusing experience. This issue was <a href="https://github.com/hotwired/turbo/issues/766">reported here</a> and it seems unclear whether this is considered a bug or not but fortunately the temporary solution offered has been effective for us:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">turbo:submit-end</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">detail</span><span class="p">.</span><span class="nx">fetchResponse</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">redirected</span> <span class="o">===</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">[type=submit]</span><span class="dl">'</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">button</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">button</span><span class="p">.</span><span class="nx">disabled</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
<p>However, just disabling a button can also be a bit of a confusing message to users so let’s style that button and even throw in a spinner so the user knows that something is really happening. For our form elements we are making use of a great gem, <a href="https://github.com/pantographe/view_component-form">view_component-form</a> which in their words:</p>
<blockquote>
<p>“provides a <code class="highlighter-rouge">FormBuilder</code> with the same interface as
<a href="https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html"><code class="highlighter-rouge">ActionView::Helpers::FormBuilder</code></a>,
but using <a href="https://github.com/github/view_component">ViewComponent</a>s
for rendering the fields. It’s a starting point for writing your own
custom ViewComponents.”</p>
</blockquote>
<p>Here is what our <code class="highlighter-rouge">ButtonComponent</code> looks like:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1"># frozen_string_literal: true</span>
<span class="k">module</span> <span class="nn">Form</span>
<span class="k">class</span> <span class="nc">ButtonComponent</span> <span class="o"><</span> <span class="no">ViewComponent</span><span class="o">::</span><span class="no">Form</span><span class="o">::</span><span class="no">ButtonComponent</span>
<span class="kp">include</span> <span class="no">IconWrapping</span>
<span class="kp">include</span> <span class="no">ButtonStyling</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">form</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="p">{})</span>
<span class="vi">@value</span> <span class="o">=</span> <span class="n">value</span>
<span class="vi">@button_type</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:type</span><span class="p">).</span><span class="nf">to_s</span>
<span class="vi">@classes</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:class</span><span class="p">)</span>
<span class="vi">@button_size</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:button_size</span><span class="p">)</span>
<span class="vi">@hide_spinner</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:hide_spinner</span><span class="p">)</span>
<span class="k">super</span><span class="p">(</span><span class="n">form</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">call</span>
<span class="n">icon</span> <span class="o">=</span> <span class="no">IconComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="ss">name: </span><span class="s1">'spinner'</span><span class="p">,</span>
<span class="ss">classes: </span><span class="s1">'hidden group-disabled:inline-block animate-spin self-center'</span>
<span class="p">)</span>
<span class="n">button_tag</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="n">icon</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">content</span> <span class="o">||</span> <span class="n">value</span><span class="p">),</span> <span class="n">options</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>There is a bit of noise you can ignore in there but the heart of the disabling with the spinner happens by adding one of our custom <code class="highlighter-rouge">IconComponent</code> ViewComponents with the classes <code class="highlighter-rouge">hidden group-disabled:inline-block animate-spin self-center</code>. Since we are injecting that spinner icon (which is just a simple inline svg) into our button, if we put a <code class="highlighter-rouge">group</code> class on our button then whenever our button gets disabled the spinner will switch from <code class="highlighter-rouge">display: hidden</code> to <code class="highlighter-rouge">display: inline-block</code> thanks to the handy <a href="https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state"><code class="highlighter-rouge">group-</code> modifier</a> provided by Tailwind. Similarly, <a href="https://tailwindcss.com/docs/animation"><code class="highlighter-rouge">animate-spin</code></a> is a succinct utility class provided by Tailwind that adds the css <code class="highlighter-rouge">animation</code> and <code class="highlighter-rouge">@keyframes</code> to get our simple spinner icon spinning.</p>
<p>Our <code class="highlighter-rouge">ButtonStyling</code> concern contains the heart of the styling of our different button variations:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">ButtonStyling</span>
<span class="nb">attr_reader</span> <span class="ss">:button_type</span><span class="p">,</span> <span class="ss">:classes</span><span class="p">,</span> <span class="ss">:button_size</span><span class="p">,</span> <span class="ss">:hide_spinner</span>
<span class="k">def</span> <span class="nf">html_class</span>
<span class="n">class_names</span><span class="p">(</span>
<span class="n">base_classes</span><span class="p">,</span>
<span class="n">type_classes</span><span class="p">,</span>
<span class="n">size_classes</span><span class="p">,</span>
<span class="n">classes</span><span class="p">,</span>
<span class="n">spinner_class</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">base_classes</span>
<span class="s1">'inline-flex gap-2 justify-center align-center font-bold rounded w-full
border-2 text-base text-center appearance-none disabled:pointer-events-none
disabled:bg-gray-400 disabled:border-gray-400 disabled:text-text-secondary'</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">type_classes</span>
<span class="k">case</span> <span class="n">button_type</span>
<span class="k">when</span> <span class="s1">'hidden'</span>
<span class="s1">'hidden'</span>
<span class="k">when</span> <span class="s1">'secondary'</span>
<span class="p">[</span>
<span class="s1">'text-primary-700 hover:text-primary-600 active:text-primary-700'</span><span class="p">,</span>
<span class="s1">'border-primary-700 hover:border-primary-600 active:border-primary-700'</span><span class="p">,</span>
<span class="s1">'hover:bg-primary-50 active:bg-primary-50'</span>
<span class="p">]</span>
<span class="k">else</span>
<span class="p">[</span>
<span class="s1">'text-white'</span><span class="p">,</span>
<span class="s1">'border-primary-600 hover:border-primary-700 active:border-primary-800'</span><span class="p">,</span>
<span class="s1">'bg-primary-600 hover:bg-primary-700 active:bg-primary-800'</span>
<span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">size_classes</span>
<span class="k">case</span> <span class="n">button_size</span>
<span class="k">when</span> <span class="s1">'small'</span>
<span class="s1">'py-2.5 px-4'</span>
<span class="k">else</span>
<span class="s1">'py-3 px-6'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">spinner_class</span>
<span class="s1">'group'</span> <span class="k">unless</span> <span class="n">hide_spinner</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Tailwind easily handles the styling of the button when disabled and we add the group class to get our animated spinner to look something like this:</p>
<p><img src="https://lh3.googleusercontent.com/pw/ADCreHeRjVUxgb-_JLgIsHy5KiV_tOB0QNx8Bph_xfE6n8nJVr8LfhvO1B_6gvuNXaT3UkIRjhD0pBek2n3o-zbEFg0osx8-TFvotDBkpzP3JIG4p5ewCFAWINEkSQU-tTSs7-t7CP3CAgSpw2afKzUnIWS_Vbw1rGUX8amWZWtV-ReSzWClEmcW6cEhIp1yBMliGOBxAgk6HRBNdENWmoN6O10YsTQEnaOxxd1sUA74jOTAWFBuvNfDW6WARS96BeFWtc2tMC-EtnqnVW7xTwl-a9zf6qCw4mP-47tYK-jf7zdKNRvU-dJhMgkOAAh_QejzfosP_OhQk5H_jbNRD4k2SwY-VKMUmwrPqjhhr-teztibEmyK-ZMQR2kO2XL84hSVo96SRy6acaBfA-XJbsflwc00luU1Px6S8J6NHZTPL9z5oXH3mQLLcZeOcB39NLOSD6Mg6v1LnsFxfct58olg4zjKUg7yhTs_5391EPbZMsmGZ5-j6D60i3U8NaqKAygd-t63l4KiIjBs8HjGvnIjzE1BSXY2Rmiq9Sl5lFukjq35IKr-RWrQiIvlY75zbbSFX5VAPXB1l3eaeR2IZLRFIloulcC7cB5xkc4feAG7YB4b-VH-smzfEVs0Nuc7kLXa_cxex0KKyGP7dr0-aLipquRvixFggFm1gXC8zXyO9MYM-AVOc_B1JtOdtTHS9exBLFNKBGTPAfrYTC-5OnH1J5a4Hp8sy0gE8qR4KSVt6WlvZE6gVaMpFCG3utHpEb_BiP58dYrIYr3BQAXwIUAoICRPCg05lJcEWwjAVnsjWWU9szNoV5KiLhPmw51qpOM2mmqH6L8GbqMyYnRLmwYu0aiaODVsG0nuREjzsxHWUQdFBOr2AHYtUaWR6T_we-sFCtdt3FQDHlX6xBwlCLsXWYGYMlkx8mvHKaN8GtScUsTWSglXnCo6on6lyg=w436-h146-no?authuser=0"" alt="Disabled Button" /></p>
<p>What if we just want to disable a button and don’t want to confuse our user with a spinner? For example, perhaps we want to prevent the user from even submitting the form until they have selected something on the page. We have that scenario covered as well with the <code class="highlighter-rouge">hide_spinner</code> option which simply skips adding the <code class="highlighter-rouge">group</code> class to the button.</p>
<p>However, we will then want to enable the spinner once the form submission and its button become active. Enabling the button is equally simple by marking our save button as a <a href="https://stimulus.hotwired.dev/reference/targets">target</a> in our <code class="highlighter-rouge">form_controller.js</code> stimulus controller that we add to just about all of our form elements and adding back the ‘group’ class:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
<span class="kd">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">saveButton</span><span class="dl">'</span><span class="p">]</span>
<span class="p">...</span>
<span class="nx">enableButton</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">hasSaveButtonTarget</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">saveButtonTarget</span><span class="p">.</span><span class="nx">disabled</span> <span class="o">=</span> <span class="kc">false</span>
<span class="k">this</span><span class="p">.</span><span class="nx">saveButtonTarget</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">group</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">disableButton</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">hasSaveButtonTarget</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">saveButtonTarget</span><span class="p">.</span><span class="nx">disabled</span> <span class="o">=</span> <span class="kc">true</span>
<span class="k">this</span><span class="p">.</span><span class="nx">saveButtonTarget</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">group</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>However, what if the conditions for enabling the button are really outside of the functional scope of the form that contains the button? We would prefer to not have to wrap the whole page in a Stimulus controller just to be able to contain all the elements that might need to communicate with our form and button.</p>
<p>Fortunately, there is a convenient way for elements to communicate across Stimulus controllers by using <a href="https://stimulus.hotwired.dev/reference/actions#global-events">global events</a>. Is we add a window data-action value of <code class="highlighter-rouge">enable-button@window->form#enableButton disable-button@window->form#disableButton</code> then we can simply run the following code from any other Stimulus controller:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">window</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="dl">'</span><span class="s1">enable-button</span><span class="dl">'</span><span class="p">));</span>
</code></pre></div></div>
<p>and our <code class="highlighter-rouge">saveButton</code> will be triggered thanks to the window event listener that Stimulus graciously “hotwired” for us.</p>
<p>Disabling buttons is a great illustration of how the Turbo and Stimulus components of the Hotwire front end framework complement each other and provide for rich user experiences with little effort. Turbo comes equipped with some basic functionality that can then be customized and built upon using lifecycle event listeners and <code class="highlighter-rouge">sprinkles</code> of Stimulus actions when that extra bit of logic is needed to complete a component or feature in your app.</p>Jonathan GreenbergDelightful Reveal #482024-02-26T14:28:30-06:002024-02-26T14:28:30-06:00repo://posts.collection/_posts/delightful-reveal-48-2024-02-26.md<p>“That’s how software works, we go into people’s dreams at night.”</p>
<h2 id="what-we-did">What we did:</h2>
<ul>
<li>Allll of the bug fixes/improvements – made a lot of progress polishing an upcoming release.</li>
<li>Allowed users to upload Attachments for specific Trainees within Training Tracker.</li>
<li>Allowed users to group a table of trainee tasks by their underlying task type.</li>
<li>Flagrant Newsletter signup forms added to the footers on the the Flagrant and Madison Ruby websites.</li>
<li>Began collaboratively planning for discovery to inform our direction for our next “epicycle” of work.</li>
<li>Welcomed Risa Goodman to the team!</li>
<li>Held a meeting about how to come to whole-company agreements about things (the Agreements Agreement).</li>
<li>Helped Risa onboard with many supportive and warm conversations.</li>
<li>Shipped designs for some Training Tracker Giveaway items.</li>
<li>Reached out to a printer to begin creating custom coloring books for Madison+ Ruby.</li>
<li>Began work on a logo rebranding.</li>
<li>Participated in QA testing for our client.</li>
<li>Jumped back into text messaging UI for a client. Updating based on feedback received from users.</li>
<li>Deployed a big feature for our client.</li>
<li>Pre-board summited to begin the conversation of 2024 goals and reflect on 2023 goals achieved or to do.</li>
<li>Madison+ Ruby social media template tweaks.</li>
<li>Announced across all our social media platforms that <a href="https://www.madisonruby.com/">Madison+ Ruby</a>’s registration is officially open and calling for sponsors!</li>
<li>Made a new Flagrant social media template.</li>
<li>Reworked a currency input to support mobile by hijacking the keypress event and entering the key strokes by hand; who needs browsers anyhow…</li>
<li>Hacked into <a href="https://github.com/paper-trail-gem/paper_trail">paper_trail</a> configuration to track price changes of services from associated content model.</li>
<li>Published an article on Authority Magazine: <a href="https://medium.com/authority-magazine/jim-remsik-of-flagrant-five-things-i-wish-someone-told-me-when-i-first-launched-my-business-or-14699acfbda2">Jim Remsik of Flagrant: Five Things I Wish Someone Told Me When I First Launched My Business or Startup</a>.</li>
<li>Interviewed with Techstrong.tv: <a href="https://techstrong.tv/videos/interviews/monolithic-microservices-flagrant-jim-remsik">Monolithic vs. Microservices with Flagrant’s Jim Remsik</a>.</li>
<li>Interviewed with Code with Jason: <a href="https://www.codewithjason.com/podcast/14490108-212-usability-testing-with-andrew-maier/">Episode 212 - Usability Testing with Andrew Maier</a></li>
<li>Published an article on SDTimes: <a href="https://sdtimes.com/softwaredev/just-make-it-pretty-tips-for-designers-when-client-feedback-is-lacking/">“Just Make it Pretty?” Tips for Designers When Client Feedback is Lacking with Kelly Rauwerdink</a></li>
</ul>
<h2 id="what-were-reading">What we’re reading:</h2>
<ul>
<li><a href="https://www.goodreads.com/en/book/show/60850767">Children of Memory</a> by Adrian Tchaikovsky</li>
<li><a href="https://app.thestorygraph.com/books/5bdcce34-bf2d-490a-af71-f0758c81931b">Children of Dune</a> by Frank Herbert</li>
<li><a href="https://app.thestorygraph.com/books/502e667c-1b5e-47c1-bb46-1f29cbff90d1">Ace: What Asexuality Reveals About Desire, Society, and the Meaning of Sex</a> by Angela Chen</li>
<li><a href="https://www.penguinrandomhouse.com/books/626187/portable-magic-by-emma-smith/">Portable Magic</a> by Emma Smith</li>
<li><a href="https://brianchristian.org/the-alignment-problem/">The Alignment Problem</a> by Brian Christian</li>
<li><a href="https://app.thestorygraph.com/books/ce6b9096-ae04-4df8-9563-bd94c78dc9d1">Everything I Never Told You</a> by Celeste Ng</li>
<li><a href="https://www.theonion.com/man-forgets-he-has-infant-strapped-to-back-1819587364">Man Forgets He Has Infant Strapped To Back</a></li>
</ul>
<h2 id="what-were-watching">What we’re watching:</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Avatar:_The_Last_Airbender_(2024_TV_series)">Avatar: The Last Airbender</a> [Live Action] (begrudgingly)</li>
<li><a href="https://en.wikipedia.org/wiki/Star_Trek:_Discovery_(season_3)">Star Trek: Discovery</a> S3</li>
<li><a href="https://en.wikipedia.org/wiki/True_Detective_(season_4)">True Detective: Night Country</a> (S4)</li>
<li><a href="https://en.wikipedia.org/wiki/The_Devil%27s_Hour">The Devil’s Hour</a></li>
<li><a href="https://en.wikipedia.org/wiki/Insecure_(TV_series)">Insecure</a> (S2)</li>
<li><a href="https://en.wikipedia.org/wiki/The_Great_Pottery_Throw_Down">The Great Pottery Throwdown</a> (S7)</li>
<li><a href="https://www.imdb.com/title/tt16283804/">One Day</a> (the TV show) +1</li>
<li><a href="https://en.wikipedia.org/wiki/Masters_of_the_Air">Masters of the Air</a></li>
<li><a href="https://en.wikipedia.org/wiki/Resident_Alien">Resident Alien</a></li>
<li><a href="https://en.wikipedia.org/wiki/Tokyo_Vice">Tokyo Vice</a> (S1)</li>
<li><a href="https://www.youtube.com/watch?v=GL33pUIwZ5U">FINAL FANTASY VII REBIRTH Gameplay Video</a></li>
<li>Accessibility sessions from <a href="https://www.deque.com/axe-con/">Axe-Con 2024</a></li>
<li><a href="https://en.wikipedia.org/wiki/Last_Week_Tonight_with_John_Oliver">Last Week Tonight</a> (S11)</li>
<li><a href="https://www.imdb.com/title/tt0051435/">The Brothers Karamazov </a></li>
</ul>
<h2 id="what-were-listening-to">What we’re listening to:</h2>
<ul>
<li><a href="https://youtu.be/ri6FaIavnWA?feature=shared">Nightwalker</a> by Ten (and Ten’s <a href="https://open.spotify.com/album/50Zo1vf3YCQtXLUZr2oBiQ?si=mbFsCHFGT1eOTgPIBVJT1A">whole album</a>) </li>
<li><a href="https://open.spotify.com/playlist/37i9dQZF1DX1ewVhAJ17m4">Pop Punk’s not dead</a> playlist on Spotify</li>
<li>Reneé Rapp</li>
<li><a href="https://podcasts.apple.com/ca/podcast/the-daily-show-ears-edition/id1334878780?i=1000645971741">The Daily Show</a></li>
<li><a href="https://www.audible.com/pd/The-Covenant-of-Water-Audiobook/B0BVDVR6XT">The Covenant Of Water</a> by Abraham Vergese</li>
<li><a href="https://youtu.be/LKqNkHZKz70?si=UHushtJYJvdv6BKI">Salt Cathedral - Terminal Woes </a></li>
<li><a href="https://open.spotify.com/episode/3xooPpTMuA9jBl5dRBNtfj?si=dfa1982049894af1">What Relationships Would You Want, if You Believed They Were Possible</a> - The Ezra Klein Show</li>
<li><a href="https://open.spotify.com/album/1eEcY3aadclBQnBcpTJGeV?si=gB9HALxNRxS7Nm71JWH0aQ">Bachelor, No. 2</a> - Aimee Mann</li>
</ul>
<h2 id="what-were-interested-in">What we’re interested in:</h2>
<ul>
<li>Photogrammetry and NYT’s <a href="https://rd.nytimes.com/">R&D in XR</a> (extended reality)</li>
<li>Efficient ways of casting Conjure Animals without slowing down the whole table</li>
<li><a href="https://www.classicfm.com/composers/cage/as-slow-a-possible-aslsp-germany-organ-chord-change/">A 639-year-long John Cage organ piece just changed chord, for the first time in two years</a> - Classic FM</li>
</ul>
<h2 id="what-were-struggling-with">What we’re struggling with:</h2>
<ul>
<li>Fish (no context)</li>
<li>Covid</li>
<li>Time.</li>
<li>Sleep</li>
<li>Blurry eyes after computer all day</li>
<li>The Louisiana legislature’s special session on crime that’s pushing a bunch of harmful stuff through to vote at lightning speed :(</li>
<li>Identifying and attempting to manage stress :(</li>
<li>wrestling with requirements and logic around when and how to apply buffers to expenses and ran into an interesting little issue with our custom decorators and how they handle an ActiveRecord_Associations_CollectionProxy vs. an ActiveRecord::Relation. </li>
<li>Fever, chills, loss of appetite</li>
<li>Boundary setting</li>
<li>AI representation of me from a prompt in Photoshop.</li>
</ul>
<!---->
<p><img src="https://lh7-us.googleusercontent.com/JDE60AZ7X2BD8Hxwm1AWo6vddL4ZWfls2pslo2G9SfpONS-m1KUKOOwHNE4PNBHxEQs5CAqIlDrHIeSn1XWWQDjVMoeQAGGwn-fA3dQ186WO5uopRyqxoRuqXaUW0fJZjD6arB8c9DeD5FarW6aXyQM" alt="" /></p>
<h2 id="what-were-buying">What we’re buying:</h2>
<ul>
<li>A new weather station for my home so I can validate what my eyes see</li>
<li>The next book in a book series</li>
<li>An over desk phone mount, so my brothers and I can play long distance Magic the Gathering. </li>
<li><a href="https://madebyelliebklyn.bigcartel.com/product/t-rex-sticker">This dino sticker</a> 🦖</li>
</ul>
<h2 id="what-were-celebrating">What we’re celebrating:</h2>
<ul>
<li>Successful glaze tests of a new recipe in the most recent kiln unload</li>
<li>Concacaf W Gold Cup tournament happening now (first year for women’s soccer 🔥)</li>
<li>Pandebonos</li>
<li>All the love we’re getting from past <a href="https://twitter.com/MadisonRuby">Madison+ Ruby attendees</a> ❤️</li>
<li>This thank you gift from Fly Media Productions</li>
</ul>
<p><img src="https://lh7-us.googleusercontent.com/eMUj6defqy4vheS6gOJnm0C5Efmo5XS4G0tPV9WxRDSe4kJbDqDjLiZJte2q1JizxCSBx_t3YX9eHnqHe_WV20sLlMMIUWsT8EhX6G-RYnNrBSQpFE-wmgH99PDpvi1QUt4SSQ3RgFvrtqMhdukWuUI" alt="" /></p>Jack PermenterA Soft Deletion Story2024-02-06T16:28:36-06:002024-02-06T16:28:36-06:00repo://posts.collection/_posts/a-soft-deletion-story-2024-02-06.md<p>To start, what is soft deletion? It’s just adding some data to a database record (like a flag or a timestamp) to mark something as “deleted”, so it remains in the database but is no longer shown. There are a few reasons to do this, but the big one is making the record easy to restore (just remove that mark-as-deleted data).</p>
<p>This is a well-known and -established pattern, and there are several packages to easily (or even automatically) handle this — or at least that’s what I’m used to from the Rails and ActiveRecord world.</p>
<p>Wait, let me clear something up right now. The manner of handling soft deletion can be contentious, mostly related to the question of just how automatically it should be dealt with. For that matter, the manner of handling deletion at all can be contentious. Actually, probably the manner of handling anything database-related can be contentious. But that’s not what this blog post is about. For now, we’re talking about soft deletion. It’s happening.</p>
<p>So, ActiveRecord. It has scopes, and a lot of things go through associations. That makes this pretty straightforward. It also has default scopes that let you do things automatically, which can be a blessing and a curse. There are really two main packages for this: <a href="https://github.com/rubysherpas/paranoia">paranoia</a> (which works on the “automatic” principle), and <a href="https://github.com/jhawthorn/discard">discard</a> (which wants you to be more explicit).</p>
<p>I’m currently on a project that’s in Elixir. I’ve been learning plenty about Elixir vs. Ruby, and Phoenix vs. Rails, and Ecto vs. ActiveRecord. On top of that, since this is my first significant Elixir project, I’m not entirely sure if anything I come across is project-specific, or if it’s a general framework thing. For instance, Ecto queries in this project are all specifying joins rather than using associations.</p>
<p>So when I looked for soft-deletion packages for Ecto and didn’t find anything compelling, I took it mostly in stride but determined I should do a little more searching. Then I found <a href="https://keathley.io/blog/soft-deletion-with-ecto.html">this blog post</a> and was intrigued by the idea of simply using views. It seemed like it would do a lot of good things without much drawback.</p>
<p>It was so stupidly simple, and what could go wrong?</p>
<ol>
<li>Create a view that’s defined as <code class="highlighter-rouge">select * from the_table where is_deleted = false;</code></li>
<li>Change the module to use the view instead of the table</li>
<li>Profit</li>
</ol>
<p>What could go wrong, indeed. At this point, let’s talk about something more specific and introduce some concrete concepts. Let’s say we have Places and Activities, and of course a Place can have multiple Activities and an Activity can be at multiple Places. This is what this would normally look like.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defmodule SoftDelete.Repo.Migrations.CreateTables do
use Ecto.Migration
def change do
create table(:places) do
add :name, :string, null: false
add :is_deleted, :boolean, null: false, default: false
end
create table(:activities) do
add :name, :string, null: false
add :type, :string
add :is_deleted, :boolean, null: false, default: false
end
create unique_index(:activities, [:name])
create table(:place_activities) do
add :place_id, references(:places), null: false
add :activity_id, references(:activities), null: false
end
create unique_index(:place_activities, [:place_id, :activity_id])
end
end
defmodule SoftDelete.Place do
use Ecto.Schema
alias SoftDelete.PlaceActivity
alias SoftDelete.Activity
schema "places" do
field :name, :string
field :is_deleted, :boolean
many_to_many :activities, Activity, join_through: PlaceActivity
end
end
defmodule SoftDelete.Activity do
use Ecto.Schema
alias SoftDelete.PlaceActivity
alias SoftDelete.Place
schema "activities" do
field :name, :string
field :type, :string
field :is_deleted, :boolean
many_to_many :places, Place, join_through: PlaceActivity
end
end
defmodule SoftDelete.PlaceActivity do
use Ecto.Schema
import Ecto.Query
alias SoftDelete.Place
alias SoftDelete.Activity
schema "place_activities" do
belongs_to :place, Place
belongs_to :activity, Activity
end
end
</code></pre></div></div>
<p>So now it’s time to go a step further and add some views.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defmodule SoftDelete.Repo.Migrations.AddSingleModelViews do
use Ecto.Migration
def up
execute """
CREATE VIEW active_places AS
SELECT * FROM places WHERE is_deleted = false;
"""
execute """
CREATE VIEW active_activities AS
SELECT * FROM activities WHERE is_deleted = false;
"""
create_if_not_exists index(:places, [:is_deleted])
create_if_not_exists index(:activities, [:is_deleted])
end
def down do
execute "DROP VIEW active_places;"
execute "DROP VIEW active_activities;"
end
end
</code></pre></div></div>
<p>And what needs to be done to the models? Literally just change the <code class="highlighter-rouge">schema</code> lines to use <code class="highlighter-rouge">active_places</code> and <code class="highlighter-rouge">active_activities</code>, respectively. That’s all.</p>
<p>This works pretty easily, in many ways. Plain queries are just working!</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Place |> Repo.all()
Place |> preload(:activities) |> Repo.all()
</code></pre></div></div>
<p>That seems like perfection, but issues come up kind of quickly. What if you don’t want to load all the activities, but just the number of activities? That’s also nice because it can be done with a single query instead of two.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Place
|> join(:left, [p], pa in PlaceActivity, on: pa.place_id == p.id)
|> group_by([p], p.id)
|> select([p, pa], %{id: p.id, activity_count: count(pa.id)})
|> Repo.all()
</code></pre></div></div>
<p>However, marking an activity as deleted doesn’t change the count. That can be fixed with another join and checking the flag, but that’s another thing to keep in mind whenever writing a query. What about another view?</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defmodule SoftDelete.Repo.Migrations.AddJoinModelView do
use Ecto.Migration
def up do
execute """
CREATE VIEW active_place_activities AS
SELECT pa.*
FROM place_activities AS pa
JOIN active_places AS ap ON pa.place_id = ap.id
JOIN active_activities AS aa ON pa.activity_id = aa.id;
"""
end
def down do
execute "DROP VIEW active_place_activities;"
end
end
</code></pre></div></div>
<p>And you know what happened with the model here, right? Yup, it’s <code class="highlighter-rouge">schema “active_place_activities”</code>.</p>
<p>This view is a little more complicated, but now we have some nice automatic stuff. It’s working nicely.</p>
<p>But I’m not enjoying getting a simple Map as a result. I’d rather get the real thing and maybe be able to preload the activities, so that’s easy: just add this count as a virtual attribute and use the struct-update syntax.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Place
|> join(:left, [p], pa in PlaceActivity, on: pa.place_id == p.id)
|> group_by([p], p.id)
|> select([p, pa], %{p | activity_count: count(pa.id)})
|> Repo.all()
</code></pre></div></div>
<script>
hljs.highlightLinesAll([
[],
[],
[],
[],
[],
[{start: 4, end: 4, color:'#D0FFBC'},],
]);
</script>
<p>Wait, what’s this?</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>** (Postgrex.Error) ERROR 42803 (grouping_error) column "a0.name" must appear in the GROUP BY clause or be used in an aggregate function
query: SELECT a0."id", a0."name", a0."is_deleted", count(a1."id") FROM "active_places" AS a0 LEFT OUTER JOIN "active_place_activities" AS a1 ON a1."place_id" = a0."id" GROUP BY a0."id"
</code></pre></div></div>
<p>Okay, this sucks. I guess I’ll add the grouping for now. And it works, but something seems fishy here. To check it out, I go directly to the database to see what’s happening.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>soft_delete_dev=# SELECT a0."id", a0."name", a0."is_deleted", count(a1."id") FROM "active_places" AS a0 LEFT OUTER JOIN "active_place_activities" AS a1 ON a1."place_id" = a0."id" GROUP BY a0."id";
ERROR: column "a0.name" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT a0."id", a0."name", a0."is_deleted", count(a1."id") F...
^
soft_delete_dev=# SELECT a0."id", a0."name", a0."is_deleted", count(a1."id") FROM "places" AS a0 LEFT OUTER JOIN "active_place_activities" AS a1 ON a1."place_id" = a0."id" GROUP BY a0."id";
id | name | is_deleted | count
----+-------------+------------+-------
1 | test one | f | 1
3 | test two | f | 1
5 | other place | f | 0
4 | some place | f | 1
(4 rows)
</code></pre></div></div>
<p>This is just the beginning of how views are troublesome. I’m not (currently) worried about the performance of the queries — how they’re automatically including where clauses and joins. What got to me is how quickly this became confusing and unhelpful.</p>
<p>When operating on a table, Postgres realizes that the primary key is special, and <a href="https://www.postgresql.org/docs/16/sql-select.html#SQL-GROUPBY">grouping by that column means only a single row will be returned</a> from that table, and the rest of the columns can be used as-is. Postgres doesn’t know this about a view.</p>
<p>Interestingly, Postgres understands this sort of “simple” (single-table) view and <a href="https://www.postgresql.org/docs/16/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS">allows writes</a> to it — inserts, updates, deletes — automatically operating on the underlying table. But just like it doesn’t know about the primary key, it doesn’t know about unique indexes. So there are situations where an <code class="highlighter-rouge">INSERT … ON CONFLICT UPDATE</code> won’t work on the view, but needs to be using the table itself. That can be dealt with by creating new copies of these models and setting the <code class="highlighter-rouge">schema</code> to the table instead of the view. And having a way to operate on the tables themselves is useful for things like a superuser or admin section of the app, where you may want to show marked-as-deleted records and restore (or actually delete) them.</p>
<p>Also, even though this is a view defined as <code class="highlighter-rouge">SELECT * FROM tablename</code>, the structure is set on creation. If you add a column to the underlying table, you have to recreate the view (which you can do in place). If you want to <em>remove</em> a column from the table, you have to first drop the view and then create it again. And don’t get me started on the join-table view. You don’t want to see the triggers I had to set up to allow writes there.</p>
<p>So that wraps up the problem statement. Stay tuned for part two of this story, where I share the solution I ended up with.</p>Yossef Mendelssohn2024 Predictions2024-01-31T10:53:49-06:002024-01-31T10:53:49-06:00repo://posts.collection/_posts/2024-predictions-2024-01-30.md<p>The year ahead offers both opportunity and pitfalls. Augmented reality (AR) is finally set to make real gains, hype around artificial intelligence (AI) will be grounded, and with an election year promising plenty of volatility, there’s no telling how markets and directions could change. That said, keep an eye on the following trends and predictions, and strap yourself in for what is sure to be a wild ride in 2024. </p>
<h1 id="ai-hype-and-changing-expectations">AI hype and changing expectations</h1>
<p>AI hype is at an all-time high. For non-technical audiences and casual observers, AI will either solve every problem society has or unleash unimaginable chaos. The fact is, AI right now is just large language models and predictive text, capable of producing valuable results, but underwhelming for those expecting sensational developments. As new solutions are introduced in 2024, and people see AI has its limits, the hype will come back to earth and expectations will become realistic.</p>
<h1 id="the-monolith-makes-a-comeback">The Monolith Makes a Comeback</h1>
<p>The monolith architecture will make a comeback and give microservices some competition 2024. When small services are built as the default they can break up before efficiency increases are understood. They’ll still be useful for new innovation, but with employee turnover, companies risk having no one versed in services written by coworkers who have moved on. A monolith, particularly in a team with shared skills and culture, will provide greater consistency in delivery, support and overall business operations.</p>
<h1 id="ahh-f-it">Ahh, “F*** it”</h1>
<p>Companies and employees are going to look for more, and take bigger chances, given the uncertainty of these times. With wars on two continents involving the U.S., and domestic attempts to disrupt the Republic, people are holding their collective breath. When faced with a disaster for too long, people eventually will say “f*** it” and move ahead. Companies and employees are tired of putting off their plans and will take action, whether that means demanding an overdue raise or launching a long stalled initiative.</p>
<h1 id="a-murky-talent-pool">A Murky Talent Pool </h1>
<p>While a market correction seems to have spurred greater availability of talent, some pros with strong skill sets still aren’t making themselves available. Having been laid off, and with money in the bank, they just haven’t needed to find another job - at least not yet. As a result, insight into the talent pool is murky at best, a scenario that will continue until companies offer better compensation packages or individual savings are drained. </p>
<h1 id="augmented-reality-takes-root">Augmented reality takes root</h1>
<p>In 2024, AR will take root in business, especially in sectors like supply change management and manufacturing. Robotics may pick products in warehouses, but human involvement is still needed. However, AR is empowering employees to do more and the applications are expanding. Additionally, consumer AR headsets will take off, driven by Apple’s entry into the market, with early adopters bringing them into the workplace as they did with the iPad. </p>
<h1 id="developers-in-demand">Developers in Demand</h1>
<p>Automation in software development aims to reduce engineering tasks, however, it’s not yet ready for prime time and gaps are showing. Automation might be useful for template type work but it can’t produce novel solutions. Further, it’s been limited to basic implementations, and a lack of understanding on how to proceed poses issues. So, regardless of automation, there will remain great demand for experienced developers.</p>Jim RemsikDelightful Reveal #462024-01-30T11:06:40-06:002024-01-30T11:06:40-06:00repo://posts.collection/_posts/delightful-reveal-46-2024-01-30.md<p>“That shipping forecast is a bop.”</p>
<h2 id="what-we-did">What we did:</h2>
<ul>
<li>Released a big feature for our client that allows tour coordinators to create quotes with custom occupancy packages.</li>
<li>Added logic to confirm leave functionality that only shows the confirmation when a user has made a change to the form.</li>
<li>Completed a more-detailed analysis of our last round of user research (on “buffering”, if you must know). Compiled four interview summaries with highlight reels, discussed takeaways, and prioritized questions for follow-on research.</li>
<li>Reviewed adoption OKRs in light of our research findings.</li>
<li>Held a bi-weekly check-in with our early adopter-in-chief, and invited her to our bi-weekly data working group to discuss data governance options while adopting the system. Wrote down the options for governing data as we understand them.</li>
<li>Compiled documents and research presentations/reports for on-boarding a product manager.</li>
<li>Sent offer letters to two candidates.</li>
<li>Implemented the search and results front-end.</li>
<li>Dug into MapBox.</li>
<li>Demoed a signup process customization proof-of-concept to a client, who enthusiastically approved following that direction.</li>
<li>Reviewed a draft of our upcoming usability research plan and identified discovery goals for our next round of research.</li>
<li>Started working on a blog post about the creation of our Flagrant Gift Boxes. :D </li>
<li>Wrapped up Training Tracker giveaway card designs, and sent them to get printed.</li>
<li>Talked with the vendor about printing issues and took steps to improve the design before the print job to allow for fewer mistakes.</li>
<li>Made progress on an internal design project.</li>
<li>Got the okay on some coffee bags to purchase.</li>
<li>Wrapped up MVP delivery on <a href="https://mobile.peoplestech.com/">A People’s History of Tech: The Mobile Phone</a>.</li>
<li>Played with some meta-programming to create an attr_initialized method that structures the initialize signature for Ruby classes and assigns instance variables in one fell swoop. </li>
<li>Worked with a <a href="https://www.houseofcolour.com/stylists/lisa-pretto-west-madison-wisconsin">House of Colour</a> stylist to determine color season and clothing ideas for headshots and public speaking.</li>
<li>Worked on Training Tracker giveaway items (separate from the cards).</li>
<li>Updating <a href="https://railsconf.org/">the RailsConf website</a> to announce the Call for Proposals is now open!</li>
<li>Started adding post themes for future announcements on the RailsConf website.</li>
<li>Connected with organizers of <a href="https://www.datatuneconf.com/">DataTune Conference</a> (March 8–9, 2024 in Nashville, TN) to talk about how to design the best experience for attendees.</li>
<li>Working with Bobbilee from <a href="https://www.lodgedout.com/">Lodged Out</a> on the preplanning for the next Flagrant Summit.</li>
</ul>
<h2 id="what-were-reading">What we’re reading:</h2>
<ul>
<li><a href="https://www.goodreads.com/book/show/31423245-how-to-be-a-stoic">How to Be a Stoic</a>, by Massimo Pigliucci</li>
<li><a href="https://www.goodreads.com/book/show/38820047-killing-commendatore">Killing Commendatore</a> by Haruki Murakami</li>
<li><a href="https://www.penguinrandomhouse.com/books/616914/an-immense-world-by-ed-yong/">An Immense World: How Animal Senses Reveal the Hidden Realms Around Us</a> by Ed Yong</li>
<li><a href="https://www.frank.computer/blog/2024/01/what-is-a-prototype.html">“What is a prototype?”</a> by Frank Elavsky</li>
<li><a href="https://cwodtke.medium.com/how-i-stopped-worrying-and-learned-to-love-design-thinking-f1142bab60e8">“How I Stopped Worrying and Learned to Love Design Thinking”</a> by Christina Wodtke</li>
<li><a href="https://en.wikipedia.org/wiki/The_Shadow_of_the_Torturer">The Shadow of the Torturer</a> by Gene Wolfe</li>
<li><a href="https://bellhooksbooks.com/product/all-about-love/">All About Love: New Visions</a> by bell hooks</li>
<li><a href="https://www.goodreads.com/book/show/60224365-before-your-memory-fades">Before Your Memory Fades</a> by Toshikazu Kawaguchi</li>
</ul>
<h2 id="what-were-watching">What we’re watching:</h2>
<ul>
<li><a href="https://www.imdb.com/title/tt17220216/">Monarch: Legacy of Monsters</a></li>
<li><a href="https://www.imdb.com/title/tt5875444/?ref_=fn_al_tt_1">Slow Horses S1</a></li>
<li><a href="https://www.imdb.com/title/tt5024912/?ref_=fn_al_tt_1">Insecure S1</a></li>
<li><a href="https://www.imdb.com/title/tt11904786/?ref_=fn_al_tt_1">Love On The Spectrum S2</a></li>
<li><a href="https://www.peacocktv.com/stream-tv/the-traitors">The Traitors</a></li>
<li>Rewatching all of the Marvel movies</li>
<li><a href="https://en.wikipedia.org/wiki/The_Bear_(TV_series)">The Bear</a></li>
<li><a href="https://en.wikipedia.org/wiki/The_Legend_of_Vox_Machina">The Legend of Vox Machina</a></li>
<li><a href="https://www.travelchannel.com/shows/the-dead-files">The Dead Files</a></li>
<li><a href="https://www.netflix.com/title/81214135">Penguin Town</a></li>
<li><a href="https://www.youtube.com/watch?v=Vs1DWYrw2Ps">Fractals, Factories, and Fast Food — Dylan Beattie</a></li>
<li><a href="https://www.imdb.com/title/tt4955642/">The Good Place</a></li>
<li><a href="https://www.disneyplus.com/series/a-real-bugs-life/4U6OnTyIVOtC">A Real Bug’s Life</a></li>
</ul>
<h2 id="what-were-listening-to">What we’re listening to:</h2>
<ul>
<li><a href="https://youtu.be/DL4UoSXwaKM?feature=shared">Prayer of The Refugee</a> by Rise Against </li>
<li><a href="https://www.youtube.com/watch?v=-tW1kqLdatA">Alma Jette - Demons</a></li>
<li><a href="https://www.youtube.com/watch?v=NFgSijY30FE">Firetrucks On The Boardwalk (Live w/ The Colorado Symphony)</a></li>
<li><a href="https://www.youtube.com/watch?v=98xgWVaipo0">What Kind of Future</a> by Woozi</li>
<li><a href="https://www.youtube.com/watch?v=3PA5FG1NX9U">MATZ</a> by HONGJOONG & SEONGHWA of Ateez</li>
<li><a href="https://www.youtube.com/watch?v=ECSZ3zy9Ifw">Rebel</a> by TVXQ!</li>
<li><a href="https://www.youtube.com/watch?v=JleoAppaxi0">Love Wins All</a> by IU</li>
<li><a href="https://www.youtube.com/watch?v=C90Cyiiz0kw">Alice in the Bluegrass</a> by Molly Tuttle</li>
<li><a href="https://youtu.be/cK9dlmYvB-Y?si=s1DcsCexTgzMH4HQ">P Stew’s Cowboy Classics</a></li>
<li><a href="https://www.youtube.com/watch?v=m-svVqefRG4">2022: 5 Hours+ of The Shipping Forecast on BBC Radio 4!</a></li>
<li><a href="https://open.spotify.com/episode/6KIdm6Fers1NrXQ3KeJGub?si=5SCLrV1IT4yNIbOEPkMecAl">Immersive Remix: “The Paper Menagerie”</a> by Ken Liu read by LeVar Burton</li>
<li><a href="https://youtu.be/x9nkiTeuYRo?feature=shared">ブルートレイン (Blue Train)</a> by Asian Kung-Fu Generation</li>
</ul>
<h2 id="what-were-interested-in">What we’re interested in:</h2>
<ul>
<li>Vantablack</li>
<li>Optimized <a href="https://tabletopbuilds.com/flagship-build-oath-of-the-watchers-paladin/">Oath of the Watchers Paladin/Hexblade Warlock/Divine Soul Sorcerer</a> multiclass</li>
<li>Getting back into digital sculpting (Zbrush/Cinema 4D)</li>
<li><a href="https://ausopen.com/">Australian Open</a></li>
<li><a href="https://www.dwell.com/article/the-life-changing-magic-of-knolling-5efe3c96">The life changing magic of knolling</a></li>
</ul>
<h2 id="what-were-struggling-with">What we’re struggling with:</h2>
<ul>
<li>Big weather changes (VA is apparently already in spring or early summer, but second winter is coming)</li>
<li>This document jumping around as people edit sections above this</li>
<li>A very stubborn home seller +1</li>
<li>Baby is overdue/late a week :(</li>
<li>Zoom deciding that this time it won’t join audio</li>
<li>Sleep +1</li>
<li>Food poisoning</li>
<li>Mortality of older pets</li>
</ul>
<h2 id="what-were-buying">What we’re buying:</h2>
<ul>
<li>Pyjamas</li>
<li>Aromatherapy / diffuser</li>
<li>Looked at, but did not purchase, a <a href="https://flomask.com/">Flomask</a></li>
</ul>
<h2 id="what-were-celebrating">What we’re celebrating:</h2>
<ul>
<li>Finally making decent sourdough bread.</li>
<li>Hiring! This is an amazing and kind team, and we’re lucky to have been in a position to hire. All of the great work that went into designing and executing a new hiring process.</li>
<li>Booking a trip to Mexico, Miami, & Key West. +1</li>
<li>70 degree day after a week of bitter cold and then heavy rains.</li>
<li>Painting! </li>
<li>Prepping prints for an upcoming art sale :)</li>
<li>huge, huge shoutout to SaVance, Jim, and Cody for working SO HARD and so many extra hours on PHOT! It looks awesome and talk about serious commitment.</li>
<li>
<p>SaVance showed a neat trick in the chrome explorer and it’s kind of a game changer!</p>
<ul>
<li>You can click on the little ‘flex’ / ‘grid’ pills, within the HTML elements tab, and that will highlight the flex / grid container and show you more clearly exactly how it is positioning its children, and gutters and such.</li>
<li>THE COOLEST part is that, it persists through refreshes, AND without having to have your cursor hovering over the elements. It is really helpful when you are flexing things multiple layers deep, and need just a smidgen of help visualizing what is going on.</li>
</ul>
</li>
</ul>Jack PermenterTurbo Confirmation Bias2024-01-25T16:16:06-06:002024-01-25T16:16:06-06:00repo://posts.collection/_posts/turbo-confirmation-bias-2024-01-10.md<p>Turbo has a knack for adding cool, little-but-useful features, resulting in a smoother experience for the user and developer alike. One lesser known capability within Turbo is the ability to easily customize the confirm process for a button. Although documented briefly in the <a href="https://turbo.hotwired.dev/handbook/drive#requiring-confirmation-for-a-visit">Turbo Handbook</a>, in order to really get an appreciation for the potential, watch <a href="https://gorails.com/episodes/custom-hotwire-turbo-confirm-modals">this excellent GoRails Videocast</a>. In this blog post, we will take things a step further and use turbo event listeners to automatically add turbo confirm dialog modals for every link on a page!</p>
<p>First you might want to take a look at the basic
<a href="https://github.com/gorails-screencasts/custom-turbo-confirm-modal/blob/master/app/javascript/application.js">ConfirmMethod</a>,
presented by Chris Oliver at GoRails. By using the <code class="highlighter-rouge">Turbo.setConfirmedMethod</code> api method, we’re able to change the default <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm"><code class="highlighter-rouge">confirm</code></a> method that Turbo would use for any form containing the <code class="highlighter-rouge">data-confirm-method</code> attribute assigned to it prior to submission.</p>
<p>We have adapted this version slightly to afford a bit more control over the content of our custom dialog:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Turbo</span><span class="p">.</span><span class="nx">setConfirmMethod</span><span class="p">((</span><span class="nx">message</span><span class="p">,</span> <span class="nx">element</span><span class="p">,</span> <span class="nx">submitter</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">dialog</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbo-confirm</span><span class="dl">"</span><span class="p">)</span>
<span class="kd">let</span> <span class="p">[</span><span class="nx">messageText</span><span class="p">,</span> <span class="nx">buttonText</span><span class="p">,</span> <span class="nx">confirmEvent</span><span class="p">]</span> <span class="o">=</span> <span class="nx">message</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">;</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">dialog</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">form</span><span class="dl">"</span><span class="p">).</span><span class="nx">method</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">dialog</span><span class="dl">'</span>
<span class="nx">dialog</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#message</span><span class="dl">"</span><span class="p">).</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">messageText</span>
<span class="nx">dialog</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#turbo-confirm</span><span class="dl">"</span><span class="p">).</span><span class="nx">textContent</span> <span class="o">=</span>
<span class="nx">submitter</span><span class="p">?.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">confirmButton</span> <span class="o">||</span> <span class="nx">buttonText</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">Confirm</span><span class="dl">'</span>
<span class="nx">dialog</span><span class="p">.</span><span class="nx">showModal</span><span class="p">()</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">dialog</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">close</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">resolved</span> <span class="o">=</span> <span class="nx">dialog</span><span class="p">.</span><span class="nx">returnValue</span> <span class="o">==</span> <span class="dl">"</span><span class="s2">confirm</span><span class="dl">"</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">resolved</span> <span class="o">&&</span> <span class="nx">confirmEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">confirmEvent</span><span class="p">}</span><span class="s2">`</span><span class="p">))</span>
<span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">resolved</span><span class="p">)</span>
<span class="p">},</span> <span class="p">{</span> <span class="na">once</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
<span class="p">})</span>
<span class="p">})</span>
</code></pre></div></div>
<p>In our application, we’re doing some unconventional Turbo get requests that require <code class="highlighter-rouge"><a></code> tags. While Turbo does support the <code class="highlighter-rouge">data-turbo-confirm</code> attribute on anchor tags, it unfortunately <a href="https://github.com/hotwired/turbo/pull/856">does not yet support access to the <code class="highlighter-rouge">submitter</code></a> — which is why we use string splitting to get more details out of our <code class="highlighter-rouge">message</code> value. We’ve also added an optional <code class="highlighter-rouge">confirmEvent</code> variable parsed from the end of the message string, triggering a dispatchEvent for those rare cases when some sort of cleanup action is needed on the page after confirmation.</p>
<p>With that all in place, we can add a <code class="highlighter-rouge">data-turbo-confirm</code> attribute to any link, button, or form on our page and get a feature rich custom styled modal dialogue that looks something like this:</p>
<p><img src="/images/uploads/turbo_confirm.png" alt="" /></p>
<p>Now, what happens when you have a page that you really don’t want your user to casually leave, perhaps via a cancel link, breadcrumbs, or any other menu navigation item on the page? You probably don’t want to litter your code with logic and conditionals for confirm attributes, which can become a lot to manage and keep up with as your design system and UI evolves over time. We found a clever hack for just this situation using the following <a href="https://stimulus.hotwired.dev/">Stimulus</a> controller:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//confirm_leave_controller.js</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
<span class="kd">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">confirmMessage</span><span class="dl">'</span><span class="p">]</span>
<span class="nx">connect</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">clickedLinks</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">this</span><span class="p">.</span><span class="nx">boundAddConfirmDialog</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">addConfirmDialog</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">turbo:click</span><span class="dl">'</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">boundAddConfirmDialog</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">disconnect</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">clickedLinks</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">link</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">link</span><span class="p">.</span><span class="nx">removeAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">data-turbo-method</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">removeEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">turbo:click</span><span class="dl">'</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">boundAddConfirmDialog</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">addConfirmDialog</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">turboConfirm</span><span class="p">,</span> <span class="nx">turboMethod</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">dataset</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">turboConfirm</span> <span class="o">=</span> <span class="nx">turboConfirm</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">turboMethod</span> <span class="o">=</span> <span class="nx">turboMethod</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">detail</span><span class="p">.</span><span class="nx">originalEvent</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">()</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">()</span>
<span class="k">this</span><span class="p">.</span><span class="nx">clickedLinks</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">)</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">click</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This controller solves the challenge of adding a data-turbo-confirm attribute on any link right after being clicked but before Turbo performs the visit. Turbo provides <a href="https://turbo.hotwired.dev/reference/events">event lifecycle hooks</a> just for these sort of use cases and we are making use of the <code class="highlighter-rouge">turbo:click</code> event to essentially hijack the event cycle entirely.</p>
<p>In <code class="highlighter-rouge">addConfirmDialog</code>, we first prevent the link’s original click event and then prevent the default <code class="highlighter-rouge">turbo-click</code> event itself in order to continue processing. That gives us a tick to add our <code class="highlighter-rouge">turboConfirm</code> data attribute while simultaneously tracking our clicked links on the page. We also ensure that our links have the <code class="highlighter-rouge">turboMethod</code> attribute required to trigger a Turbo click visit. Finally, we end by programmatically clicking the link again so that the turbo confirm process can happen. Note that detecting the <code class="highlighter-rouge">turboConfirm</code> attribute occurs much too early in the visit process, which is why we need to abandon and then reboot the click event for the link.</p>
<p>You probably notice that we do a little cleanup on the clicked links removing the <code class="highlighter-rouge">turboMethod</code> attribute on links that have been clicked. This is because we might have refreshed part of the page with a <a href="https://turbo.hotwired.dev/handbook/frames">Turbo Frame</a> or a <a href="https://turbo.hotwired.dev/handbook/streams">Turbo Stream</a> and have therefore intentionally disconnected our <code class="highlighter-rouge">confirm-leave</code> controller since the state of the page no longer requires a confirm dialog — and we don’t want any links outside of the frame to continue bothering our user. Turbo does a lot of magic observing and converting our links into forms. Adding and then removing the <code class="highlighter-rouge">data-turbo-method</code> seems to be required to both initiate the confirm process after our hijack and to release it when we no longer want the confirm behavior.</p>
<p>Developers and users alike have long detested the clunky default confirm dialog messages offered by browsers, and we’re thankful that Turbo has provided us a way to improve this user experience. In addition, Turbo provides just enough methods, settings, and hooks to customize that experience further and — with a bit of tinkering — we can build enhancements like our <code class="highlighter-rouge">confirm-leave</code> controller, providing even richer experiences and greater developer ease going forward.</p>Jonathan GreenbergUp Your Design Game2024-01-17T17:22:24-06:002024-01-17T17:22:24-06:00repo://posts.collection/_posts/up-your-design-game-2024-01-11.md<p>Every now and then I hear someone say something that reminds me how much design and what designers do can seem like a <a href="https://en.wikipedia.org/wiki/Black_box">black box</a> to people outside our profession. How do designers know what looks good? Isn’t design just one of those things that people are innately good at—either you have a feel for it or you don’t?</p>
<p>Well, no!</p>
<p>Design is a skill. Designers learn principles and best practices. We acquire knowledge about how best to design something just like anyone else—through curiosity, reading, educational resources, and lots and lots of practice.</p>
<p>Sharpen your design skills and nerd out with some of my favorite design books:</p>
<p><strong><a href="https://app.thestorygraph.com/books/ce8ab59f-e365-4081-8f18-96e7434dad52"><em>The Anatomy of Type</em> by Stephen Coles</a></strong></p>
<p>Ever want to learn all those nerdy words about type? Develop your typographic eye with visual diagrams of letters and come away <a href="https://www.monotype.com/resources/z-typographic-terms">with vocabulary like “shoulder”, “tail”, and “aperture”</a> to describe typefaces. The book examines 100 typefaces and their full character sets, annotating key features and anatomical details that make them distinct. Learn to separate Humanist Sans from Gothic Sans and how different styles can impact designs when choosing a typeface.</p>
<p><strong><a href="https://app.thestorygraph.com/books/c9468c52-2faa-4345-a393-6679d2ac7450"><em>Thinking with Type</em> by Ellen Lupton</a></strong></p>
<p>More concerned with type decisions like layout, font type, size, alignment, and spacing? Thinking with Type zooms out from the anatomy of a character and provides guidance for working with typography—whether on the page or the screen. Lupton provides key design principles to guide your decisions as a designer working with type.</p>
<p><strong><a href="https://app.thestorygraph.com/books/3667115d-c9a3-494e-b52d-de77777bf415"><em>Design for Hackers</em> by David Kadavy</a></strong></p>
<p>As a user experience designer, I often work with folks who are not designers, but who want to better understand design as a discipline without jumping into the deep end. This book is a great 101-style, high-level overview of things like proportion, color theory, composition, typography and other Big Topics designers concern themselves with, but in a highly digestible, skimmable format. It’s a great book to use as a jumping off point for deeper study.</p>
<p><strong><a href="https://app.thestorygraph.com/books/0b6fc6dd-6664-4435-8b49-4dde0efd4f05"><em>In Progress</em> by Jessica Hische</a></strong></p>
<p>You’ve probably seen <a href="http://jessicahische.is/">Jessica Hische</a>’s lettering work before <a href="https://www.jessicahische.is/working">on movie posters, Wes Anderson film credits, Penguin Classics books, postage stamps, or Starbucks and Target gift cards</a>. In this book, she shares her creative and technical processes for lettering and vectorizing hand-drawn letters. Loaded with images from pencil sketch to computer screen, this one’s essential for understanding how to move from concept to vectorized file efficiently without losing the magic that makes hand-drawn lettering special.</p>
<p><strong><a href="https://app.thestorygraph.com/books/5737668c-3d97-43cd-82c1-ab36c5d38989"><em>Design of Everyday Things</em> by Donald Norman</a></strong></p>
<p>Design isn’t just how something looks, but how it works, how it’s used, and how humans interact with it. This book will forever change perceptions of your designed surroundings. For example, have you ever pulled a door handle only to discover it’s a “push” door with a label that says so? That’s a “<a href="https://99percentinvisible.org/article/norman-doors-dont-know-whether-push-pull-blame-design/">Norman Door</a>” (named for this book’s author). You made the mistake not because you weren’t paying attention, but because the door’s design was bad. You’ll never look at doors the same way again!</p>
<p><strong><a href="https://app.thestorygraph.com/books/63f60e39-df40-4a9b-a6cb-cc701f8c0329"><em>The Secret Lives of Color</em> by Kassia St. Clair</a></strong></p>
<p>A narrative history of pigments and how humans have discovered and used them throughout time. Stuff your brain with trivia about expensive dyes, toxic hues, and weird ingredients for making color. <a href="https://en.wikipedia.org/wiki/Tyrian_purple">Imperial purple</a> extracted from the tiny mucus glands of snails! <a href="https://en.wikipedia.org/wiki/Scheele%27s_Green#Toxicity">Green wallpaper</a> that might have killed Napoleon! When we finally stopped grinding up mummies to make <a href="https://en.wikipedia.org/wiki/Mummy_brown">Mummy Brown</a>! Learn about all these weird color facts in this book.</p>Glynnis Ritchie