<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/css" href="https://www.aaron-gustafson.com/c/feed.min.css" ?><feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:amg="https://www.aaron-gustafson.com.com/amg-dtd/"><title>Aaron Gustafson: Content tagged JavaScript</title><subtitle>The latest 20 posts and links tagged JavaScript.</subtitle><id>https://www.aaron-gustafson.com</id><link href="https://www.aaron-gustafson.com/feeds/javascript.xml" rel="self"/><link href="https://www.aaron-gustafson.com"/><author><name>Aaron Gustafson</name><uri>https://www.aaron-gustafson.com</uri></author><updated>2026-01-31T00:04:36Z</updated><entry><id>https://www.aaron-gustafson.com/notebook/repeatable-form-fields-made-simple/</id><title type="html"><![CDATA[✍🏻 Repeatable Form Fields Made Simple]]></title><link href="https://www.aaron-gustafson.com/notebook/repeatable-form-fields-made-simple/" rel="alternate" type="text/html" /><published>2026-01-31T00:04:36Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Sometimes you need users to provide multiple instances of the same information—multiple email addresses, phone numbers, team members, or emergency contacts. The <code>form-repeatable</code> web component makes this straightforward, handling field duplication, automatic renumbering, and seamless form submission via the ElementInternals API.</p><p>All you need to do is provide a single field group and the component handles the rest:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>stop-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Stop 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>stop-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>stops[]<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>The <code>form-repeatable</code> component treats its first child as a template and injects a <code>button</code> that allows users to repeat the field. When users click “Add Another” (the default “add” button text), the following happens:</p><ol><li>The template is cloned,</li><li>Any numbers are auto-incremented (“Stop 1” → “Stop 2”, <code>stop-1</code> → <code>stop-2</code>),</li><li>A new group is added to the component,</li><li>A “remove” button is added when there’s more than the minimum number of groups (1 by default), and</li><li>Form values update automatically via <code>ElementInternals</code>.</li></ol><p>That last piece is crucial. The plugin is a fully-participating member in the parent form:</p><ul><li>All inputs are collected and submitted automatically</li><li>Values are added to <code>FormData</code></li><li>In-built form reset is respected</li><li>A form’s disabled state is respected</li></ul><p>No special handling required — it works like any native form control.</p><h2 id="need-customized-buttons%3F-you-bet!" tabindex="-1"><a class="header-anchor" href="#need-customized-buttons%3F-you-bet!" aria-hidden="true">#</a> Need customized buttons? You bet!</h2><p>If you don’t like the default text or your site is in another language — no biggie! You can define your own button labels using the <code>add-label</code> and <code>remove-label</code> attributes:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token attr-name">add-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Add Another Item<span class="token punctuation">”</span></span><span class="token attr-name">remove-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Delete<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>item-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>item-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>items[]<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>With that simple change, the add button reads “Add Another Item” and each remove button reads “Delete”. To improve the experience for screen reader users, the <code>remove-label</code> value is combined with the associated label/legend to create accessible names like “Delete Item 1” which is far more helpful.</p><h2 id="already-have-values-to-show%3F-no-problem." tabindex="-1"><a class="header-anchor" href="#already-have-values-to-show%3F-no-problem." aria-hidden="true">#</a> Already have values to show? No problem.</h2><p>If your form needs to start with multiple groups already filled in, just provide them as child elements:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>tel<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phones[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>555-0100<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-2<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-2<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>tel<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phones[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>555-0101<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-3<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-3<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>tel<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phones[]<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>All the children will become groups managed by the component and their existing values are preserved. Perfect progressive enhancement!</p><h2 id="need-to-do-something-a-little-more-complex%3F-i-got-you." tabindex="-1"><a class="header-anchor" href="#need-to-do-something-a-little-more-complex%3F-i-got-you." aria-hidden="true">#</a> Need to do something a little more complex? I got you.</h2><p>You’re not limited to repeating a single field. Each group can contain multiple, related fields. Here’s an example with a <code>fieldset</code> for guest information:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Guest 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-name-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-name-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-name-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>When this gets picked up by the component, the whole <code>fieldset</code> will become the template. When users add new Guests, all of the numeric values — whether in text or attributes — increment automatically when new groups are added. So in this case, the <code>legend</code> will update, as will the <code>for</code> attribute on the <code>label</code> and <code>id</code> and <code>name</code> attributes on the <code>input</code>.</p><h2 id="need-to-constrain-the-responses%3F-you-got-it." tabindex="-1"><a class="header-anchor" href="#need-to-constrain-the-responses%3F-you-got-it." aria-hidden="true">#</a> Need to constrain the responses? You got it.</h2><p>Use the <code>min</code> and <code>max</code> attributes to control the number of allowed groups:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<span class="token punctuation">”</span></span><span class="token attr-name">max</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>5<span class="token punctuation">”</span></span><span class="token attr-name">add-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Add Team Member<span class="token punctuation">”</span></span><span class="token attr-name">remove-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Remove<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>member-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Team Member 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>member-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>members[]<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>This creates a component that:</p><ul><li>Starts with 1 member,</li><li>Requires adding new members until the <code>min</code> threshold (2) is met,</li><li>Cannot have fewer than 2 team members,</li><li>Cannot have more than 5 team members, and</li><li>Uses custom button labels.</li></ul><p>The remove buttons are not shown when at the minimum threshold (1 by default) and the add button disappears when you hit the maximum.</p><h2 id="prefer-an-explicit-template%3F-bring-it!" tabindex="-1"><a class="header-anchor" href="#prefer-an-explicit-template%3F-bring-it!" aria-hidden="true">#</a> Prefer an explicit <code>template</code>? Bring it!</h2><p>This component can accept a <code>template</code> element containing the fields you want to repeat. Just drop in <code>{n}</code> placeholders where you want the sequential numbers to appear:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email-{n}<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Email {n}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email-{n}<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>emails[]<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>When hoisted into the component, the <code>template</code> element is removed from the light DOM and used internally.</p><h2 id="here%E2%80%99s-what-you-need-to-know-about-styling-it" tabindex="-1"><a class="header-anchor" href="#here%E2%80%99s-what-you-need-to-know-about-styling-it" aria-hidden="true">#</a> Here’s what you need to know about styling it</h2><p>The component uses Shadow DOM to encapsulate its internal structure, but you can style it using CSS parts and custom properties. It also adopts your global styles automatically.</p><p>The component uses CSS Grid by default:</p><ul><li><strong>Two columns</strong>: Content in column 1, remove buttons aligned inline end in column 2</li><li><strong>Subgrid</strong>: Each group uses <code>subgrid</code> to align with parent grid</li><li><strong>Add button</strong>: Appears below all groups</li></ul><p>You can use CSS parts to style the buttons and field groups. Here are some examples:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token comment">/* Style all buttons <em>/</span><span class="token selector">form-repeatable::part(button)</span><span class="token punctuation">{</span><span class="token property">padding</span><span class="token punctuation">:</span> 0.5rem 1rem<span class="token punctuation">;</span><span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span><span class="token property">cursor</span><span class="token punctuation">:</span> pointer<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style the add button <em>/</span><span class="token selector">form-repeatable::part(add-button)</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #28a745<span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style remove buttons <em>/</span><span class="token selector">form-repeatable::part(remove-button)</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #dc3545<span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Customize the grid layout <em>/</span><span class="token selector">form-repeatable::part(groups)</span><span class="token punctuation">{</span><span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 1fr auto<span class="token punctuation">;</span><span class="token property">gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style each group */</span><span class="token selector">form-repeatable::part(group)</span><span class="token punctuation">{</span><span class="token property">padding</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token property">background</span><span class="token punctuation">:</span> #f8f9fa<span class="token punctuation">;</span><span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span><span class="token property">margin-bottom</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>Available parts:</p><ul><li><code>groups</code> - Container for all groups (CSS grid by default)</li><li><code>group</code> - Each repeatable group wrapper</li><li><code>content</code> - Container for group’s fields</li><li><code>group-controls</code> - Container for the remove button</li><li><code>controls</code> - Container for the add button</li><li><code>button</code> - All buttons</li><li><code>add-button</code> - The add button</li><li><code>remove-button</code> - All remove buttons</li></ul><h2 id="want-to-keep-a-watchful-eye%3F-you%E2%80%99re-extra%2C-but-sure." tabindex="-1"><a class="header-anchor" href="#want-to-keep-a-watchful-eye%3F-you%E2%80%99re-extra%2C-but-sure." aria-hidden="true">#</a> Want to keep a watchful eye? You’re extra, but sure.</h2><p>You can listen for when groups are added or removed and run your own custom code:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> repeatable <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-repeatable”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>repeatable<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-repeatable:added”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Group added. Total groups:”</span><span class="token punctuation">,</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>groupCount<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>repeatable<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-repeatable:removed”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Group removed. Total groups:”</span><span class="token punctuation">,</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>groupCount<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="go-with-the-progressive-enhancement-flow" tabindex="-1"><a class="header-anchor" href="#go-with-the-progressive-enhancement-flow" aria-hidden="true">#</a> Go with the progressive enhancement flow</h2><p>If JavaScript fails, users see the initial field group(s) and can fill them in. They can’t add more, but nothing breaks. Make sure your minimum count accommodates users without JavaScript.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>Explore <a href="https://aarongustafson.github.io/form-repeatable/demo/">the demo</a> with various examples:</p><figure id="fig-2025-12-06-07" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-repeatable/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it-now" tabindex="-1"><a class="header-anchor" href="#grab-it-now" aria-hidden="true">#</a> Grab it now</h2><p>Check out the project on <a href="https://github.com/aarongustafson/form-repeatable">GitHub</a>. Install via npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-repeatable</code></pre><p>Import and go:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/form-repeatable”</span><span class="token punctuation">;</span></code></pre><p>This single component instance manages all your repeatable field groups with native form participation — no framework required.</p>]]></content><amg:twitter><![CDATA[Add repeatable form field groups with automatic numbering and native form participation.]]></amg:twitter><amg:summary><![CDATA[Need to let users add multiple email addresses, phone numbers, or other field groups? The <code>form-repeatable</code> web component handles the duplication, renumbering, and form submission automatically.]]></amg:summary><summary type="html"><![CDATA[<p>Need to let users add multiple email addresses, phone numbers, or other field groups? The <code>form-repeatable</code> web component handles the duplication, renumbering, and form submission automatically.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-starter-template/</id><title type="html"><![CDATA[✍🏻 A Production-Ready Web Component Starter Template]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-starter-template/" rel="alternate" type="text/html" /><published>2026-01-02T00:06:37Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Creating a new web component from scratch involves a lot of boilerplate—testing setup, build configuration, linting, CI/CD, documentation structure, and more. After building — and refining/rebuilding — numerous web components, I’ve distilled all that work into a starter template that lets you focus on your component’s functionality rather than project setup.</p><p>The <a href="https://github.com/aarongustafson/web-component-starter">Web Component Starter Template</a> is based on the architecture and patterns I’ve refined across my web component work, incorporating <a href="https://web.dev/articles/custom-elements-best-practices">Google’s Custom Element Best Practices</a> and advice from other web components practitioners including the always-brilliant <a href="https://daverupert.com/">Dave Rupert</a>.</p><h2 id="what%E2%80%99s-included" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-included" aria-hidden="true">#</a> What’s included</h2><p>The template provides everything you need to create a production-ready web component:</p><ul><li><strong>Interactive setup wizard</strong> that scaffolds everything for your component.</li><li><strong>Multiple import patterns</strong> supporting both auto-define and manual registration.</li><li><strong>Demo pages</strong> for development, documentation, and CDN examples.</li><li><strong>Code quality tools</strong> including ESLint and Prettier with sensible defaults.</li><li><strong>Modern testing setup</strong> with Vitest, Happy DOM, and coverage reporting.</li><li><strong>CI/CD workflows</strong> for GitHub Actions with automated testing and npm publishing.</li><li><strong>Publishing ready</strong> with proper npm package configuration and OIDC support.</li></ul><h2 id="quick-start-with-interactive-setup" tabindex="-1"><a class="header-anchor" href="#quick-start-with-interactive-setup" aria-hidden="true">#</a> Quick start with interactive setup</h2><p>Getting started is straightforward. If you’re a GitHub user, you can create a new repository directly from the template. Alternatively, clone it locally:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> clone <a href="https://github.com/aarongustafson/web-component-starter.git">https://github.com/aarongustafson/web-component-starter.git</a> my-component<span class="token builtin class-name">cd</span> my-component<span class="token function">npm</span><span class="token function">install</span><span class="token function">npm</span> run setup</code></pre><p>The setup wizard asks for your component name and description, then automatically:</p><ul><li>Renames all files based on your component name,</li><li>Updates all code and configuration templates with your details,</li><li>Generates a proper README from the included template,</li><li>Cleans up all template-specific files, and</li><li>Initializes the git repository.</li></ul><p>You’re left with a fully scaffolded repository, ready for you to develop your component.</p><h2 id="flexible-import-patterns" tabindex="-1"><a class="header-anchor" href="#flexible-import-patterns" aria-hidden="true">#</a> Flexible import patterns</h2><p>One of the key features is support for multiple registration patterns. Users of your component can choose what works best:</p><p><strong>Manual registration for full control:</strong></p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> ComponentNameElement <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">‘@yourscope/component-name’</span><span class="token punctuation">;</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">‘my-custom-name’</span><span class="token punctuation">,</span> ComponentNameElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p><strong>Auto-define for convenience:</strong></p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">‘@yourscope/component-name/define.js’</span><span class="token punctuation">;</span></code></pre><p><strong>Or call the helper directly:</strong></p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> defineComponentName <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">‘@yourscope/component-name/define.js’</span><span class="token punctuation">;</span><span class="token function">defineComponentName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>The auto-define approach includes guards to ensure it only runs in browser environments and checks if <code>customElements</code> is available, making it safe for server-side rendered (SSR) scenarios.</p><h2 id="testing-made-easy" tabindex="-1"><a class="header-anchor" href="#testing-made-easy" aria-hidden="true">#</a> Testing made easy</h2><p>The template includes a comprehensive testing setup using Vitest:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> describe<span class="token punctuation">,</span> it<span class="token punctuation">,</span> expect <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">‘vitest’</span><span class="token punctuation">;</span><span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">‘MyComponent’</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">‘should render’</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">const</span> el <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">‘my-component’</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token function">expect</span><span class="token punctuation">(</span>el<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBeInstanceOf</span><span class="token punctuation">(</span>HTMLElement<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Happy DOM provides a lightweight browser environment, and the included scripts support:</p><ul><li>Watch mode for development: <code>npm test</code></li><li>Single run for CI: <code>npm run test:run</code></li><li>Interactive UI: <code>npm run test:ui</code></li><li>Coverage reports: <code>npm run test:coverage</code></li></ul><h2 id="automated-publishing-with-oidc" tabindex="-1"><a class="header-anchor" href="#automated-publishing-with-oidc" aria-hidden="true">#</a> Automated publishing with OIDC</h2><p>The template is configured for secure automated publishing to npm using OpenID Connect (OIDC), which is more secure than long-lived tokens. After you manually publish the first version and configure OIDC on npm, create a GitHub release and the workflow handles publishing automatically.</p><p>Manual publishing is still supported if you prefer that approach.</p><h2 id="following-best-practices" tabindex="-1"><a class="header-anchor" href="#following-best-practices" aria-hidden="true">#</a> Following best practices</h2><p>The template bakes in best practices from the start:</p><ul><li>Shadow DOM with proper encapsulation</li><li>Custom Elements v1 API</li><li>Reflection of properties to attributes</li><li>Lifecycle callbacks used appropriately</li><li>Accessible patterns and ARIA support</li><li>Progressive enhancement approach</li></ul><p>The included <a href="https://github.com/aarongustafson/web-component-starter/blob/main/WEB-COMPONENTS-BEST-PRACTICES.md"><code>WEB-COMPONENTS-BEST-PRACTICES.md</code> document</a> explains the reasoning behind each pattern, making it a learning resource as well as a starter template.</p><h2 id="why-i-built-this" tabindex="-1"><a class="header-anchor" href="#why-i-built-this" aria-hidden="true">#</a> Why I built this</h2><p>After creating components like <a href="https://github.com/aarongustafson/form-obfuscator">form-obfuscator</a>, <a href="https://github.com/aarongustafson/tabbed-interface">tabbed-interface</a>, and several others, I found myself copying and adapting the same project structure each time. This template captures those patterns so I — and now you — can start building components faster.</p><p>If you build something with it, I’d love to hear about it!</p>]]></content><amg:twitter><![CDATA[Start building web components the right way with this production-ready template.]]></amg:twitter><amg:summary><![CDATA[I created a comprehensive starter template for creating production-ready web components with modern tooling, testing, and CI/CD—following Google's Custom Element Best Practices.]]></amg:summary><summary type="html"><![CDATA[<p>I created a comprehensive starter template for creating production-ready web components with modern tooling, testing, and CI/CD—following Google's Custom Element Best Practices.</p>]]></summary><category term="web components" /><category term="JavaScript" /><category term="open source" /><category term="developer tools" /><category term="best practices" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/dynamic-datalist-autocomplete-from-an-api/</id><title type="html"><![CDATA[✍🏻 Dynamic Datalist: Autocomplete from an API]]></title><link href="https://www.aaron-gustafson.com/notebook/dynamic-datalist-autocomplete-from-an-api/" rel="alternate" type="text/html" /><published>2025-12-16T19:46:29Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>HTML’s <code>datalist</code> element provides native autocomplete functionality, but it’s entirely static—you have to know all the options up front. The <code>dynamic-datalist</code> web component solves this by fetching suggestions from an API endpoint as users type, giving you the benefits of native autocomplete with the flexibility of dynamic data.</p><p>This component is a modern replacement for <a href="https://github.com/easy-designs/jquery.easy-predictive-typing.js">my old jQuery predictive typing plugin</a>. I’ve reimagined it as a standards-based web component.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>To use the component, wrap it around your <code>input</code> field and specify an endpoint:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Search<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Type to search…<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>As users type, the component makes GET requests to that endpoint, passing in the typed value as the “query” parameter (e.g., <code>/api/search?query=WHAT_THE_USER_TYPED</code>). The response from the endpoint is used to populates a dynamic <code>datalist</code> element with the results.</p><p>The structure of the response should be JSON with an <code>options</code> array of string values:</p><pre class="language-json" tabindex="0"><code class="language-json"><span class="token punctuation">{</span><span class="token property">“options”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token string">“option 1”</span><span class="token punctuation">,</span><span class="token string">“option 2”</span><span class="token punctuation">,</span><span class="token string">“option 3”</span><span class="token punctuation">]</span><span class="token punctuation">}</span></code></pre><h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works" aria-hidden="true">#</a> How it works</h2><p>Under the hood, the component:</p><ol><li>Adopts (or creates) a <code>datalist</code> element for your <code>input</code>,</li><li>Listens for “input” events,</li><li>Debounces requests (waiting at least 250ms) to avoid overwhelming your API,</li><li>Sends requests to your endpoint with the current value of the <code>input</code>,</li><li>Reads back the JSON response,</li><li>Updates the <code>datalist</code><code>option</code> elements, and</li><li>Dispatches the update event.</li></ol><p>All of this happens transparently—users just see autocomplete suggestions appearing as they type.</p><h2 id="need-post%3F" tabindex="-1"><a class="header-anchor" href="#need-post%3F" aria-hidden="true">#</a> Need POST?</h2><p>You can change the submission method via the <code>method</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/lookup<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>lookup<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Lookup<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>lookup<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>lookup<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>This sends a POST request with a JSON body: <code>{ &quot;query&quot;: &quot;…&quot; }</code>. Currently GET and POST are supported, but I could add more if folks want them.</p><h2 id="custom-variable-names" tabindex="-1"><a class="header-anchor" href="#custom-variable-names" aria-hidden="true">#</a> Custom variable names</h2><p>As I mentioned, the component uses “query” as the parameter name by default, but you can easily change it via the <code>key</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/terms<span class="token punctuation">”</span></span><span class="token attr-name">key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>term<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Term search<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>term<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>This sends the GET request <code>/api/terms?term=…</code>.</p><h2 id="working-with-existing-datalists" tabindex="-1"><a class="header-anchor" href="#working-with-existing-datalists" aria-hidden="true">#</a> Working with existing datalists</h2><p>If your <code>input</code> already has a <code>datalist</code> defined, the component will inherit it and replace the existing options with the fetched results, which makes for a nice progressive enhancement:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/cities<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>city<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>City<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>city<span class="token punctuation">”</span></span><span class="token attr-name">list</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>cities-list<span class="token punctuation">”</span></span><span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Type a city…<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>datalist</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>cities-list<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>New York<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>Los Angeles<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>Chicago<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>datalist</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>Users see the pre-populated cities immediately, and as they type, API results supplement the list. If JavaScript fails or the web component doesn’t load, users still get the static options. Nothing breaks.</p><h2 id="event-handling" tabindex="-1"><a class="header-anchor" href="#event-handling" aria-hidden="true">#</a> Event handling</h2><p>If you want to tap into the component’s event system, it fires three custom events:</p><ul><li><code>dynamic-datalist:ready</code> - Fired when the component initializes</li><li><code>dynamic-datalist:update</code> - Fired when the <code>datalist</code> is updated with new options</li><li><code>dynamic-datalist:error</code> - Fired when an error occurs fetching data</li></ul><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> element <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:ready”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Component ready:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:update”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Options updated:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:error”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">“Error:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Each event provides helpful <code>detail</code> objects with references to the <code>input</code>, <code>datalist</code>, and other relevant data.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>Check out <a href="https://aarongustafson.github.io/dynamic-datalist/demo/">the demo</a> for live examples (there are also <a href="https://aarongustafson.github.io/dynamic-datalist/demo/unpkg.html">unpkg</a> and <a href="https://aarongustafson.github.io/dynamic-datalist/demo/esm.html">ESM</a> builds if you want to test CDN delivery):</p><figure id="fig-2025-12-06-03" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/dynamic-datalist/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>The project is available on <a href="https://github.com/aarongustafson/dynamic-datalist">GitHub</a>. You can also install via npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/dynamic-datalist</code></pre><p>If you go that route, there are a few ways to register the element depending on your build setup:</p><h3 id="option-1%3A-define-it-yourself" tabindex="-1"><a class="header-anchor" href="#option-1%3A-define-it-yourself" aria-hidden="true">#</a> Option 1: Define it yourself</h3><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> DynamicDatalistElement <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/dynamic-datalist”</span><span class="token punctuation">;</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist”</span><span class="token punctuation">,</span> DynamicDatalistElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="option-2%3A-let-the-helper-guard-registration" tabindex="-1"><a class="header-anchor" href="#option-2%3A-let-the-helper-guard-registration" aria-hidden="true">#</a> Option 2: Let the helper guard registration</h3><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/dynamic-datalist/define.js”</span><span class="token punctuation">;</span><span class="token comment">// or, when you need to wait:</span><span class="token keyword">import</span><span class="token punctuation">{</span> defineDynamicDatalist <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/dynamic-datalist/define.js”</span><span class="token punctuation">;</span><span class="token function">defineDynamicDatalist</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="option-3%3A-drop-the-helper-in-via-a-%3Cscript%3E-tag" tabindex="-1"><a class="header-anchor" href="#option-3%3A-drop-the-helper-in-via-a-%3Cscript%3E-tag" aria-hidden="true">#</a> Option 3: Drop the helper in via a <code>&lt;script&gt;</code> tag</h3><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>./node_modules/@aarongustafson/dynamic-datalist/define.js<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>module<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span></code></pre><p>Regardless of how you register it, there are no framework dependencies—just clean autocomplete powered by your API. As I mentioned, it’s also available via CDNs, such as unpkg too, if you’d prefer to go that route.</p>]]></content><amg:twitter><![CDATA[Want API-driven autocomplete suggestions in your forms? Here’s a web component that makes it happen.]]></amg:twitter><amg:summary><![CDATA[The <code>datalist</code> element is great for autocomplete, but it's static. The <code>dynamic-datalist</code> web component brings dynamic, API-driven suggestions to your text fields as users type.]]></amg:summary><summary type="html"><![CDATA[<p>The <code>datalist</code> element is great for autocomplete, but it's static. The <code>dynamic-datalist</code> web component brings dynamic, API-driven suggestions to your text fields as users type.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><category term="API" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-for-obfuscating-form-fields/</id><title type="html"><![CDATA[✍🏻 A Web Component for Obfuscating Form Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-for-obfuscating-form-fields/" rel="alternate" type="text/html" /><published>2025-12-06T20:03:47Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>We have the password reveal pattern for passwords, but what about other sensitive fields that need to be readable while editing and obfuscated while at rest? The <code>form-obfuscator</code> web component does exactly that.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>Wrap any text field in the component and it will automatically obfuscate the value when the field loses focus:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>secret-key-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>What was your first pet’s name?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>secret-key-1<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>secret-key-1<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>When users click into the field, they see the actual value. When they click away, it’s replaced with asterisks (<em>). The real value is preserved in a hidden field for form submission.</p><h2 id="custom-obfuscation-characters" tabindex="-1"><a class="header-anchor" href="#custom-obfuscation-characters" aria-hidden="true">#</a> Custom obfuscation characters</h2><p>If you don’t like asterisks, you can specify any character you like:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">character</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>•<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>account<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Account Number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>account<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>account<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>Or get creative:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">character</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>🤐<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Social Security Number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><h2 id="pattern-based-obfuscation" tabindex="-1"><a class="header-anchor" href="#pattern-based-obfuscation" aria-hidden="true">#</a> Pattern-based obfuscation</h2><p>Sometimes you want to show part of the value while hiding the rest. The <code>pattern</code> attribute lets you specify which characters to keep visible:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>\d{4}$<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Social Security Number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This keeps the last four digits visible while replacing everything else with your obfuscation character. Perfect for Social Security Numbers, credit cards, or phone numbers where showing the last few digits helps users confirm they’ve entered the right value.</p><h2 id="limiting-displayed-characters" tabindex="-1"><a class="header-anchor" href="#limiting-displayed-characters" aria-hidden="true">#</a> Limiting displayed characters</h2><p>Use the <code>maxlength</code> attribute to cap how many characters appear when obfuscated:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">maxlength</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>4<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Password<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>Even if the user enters a 20-character value, only four asterisks will be displayed when the field is obfuscated. This prevents giving away information about the length of the information entered.</p><h2 id="custom-replacement-functions" tabindex="-1"><a class="header-anchor" href="#custom-replacement-functions" aria-hidden="true">#</a> Custom replacement functions</h2><p>For complete control, you can provide a JavaScript function via the <code>replacer</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript">window<span class="token punctuation">.</span><span class="token function-variable function">emailReplacer</span><span class="token operator">=</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> username <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">var</span> domain <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">return</span> username<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">.</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span><span class="token string">&quot;</em>“</span><span class="token punctuation">)</span><span class="token operator">+</span> domain<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>^(.<em>?)(@.+)$<span class="token punctuation">“</span></span><span class="token attr-name">replacer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>return emailReplacer(arguments)<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Email Address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>text<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span><a href="mailto:user@example.com">user@example.com</a><span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This example uses a pattern to separate the username from the domain, then obfuscates only the username portion, leaving <code>@example.com</code> visible.</p><p>Here’s another practical example for credit cards:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript"><span class="token keyword">function</span><span class="token function">cardNumberReplacer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> beginning <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">var</span> final_digits <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">return</span> beginning<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\d</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span><span class="token string">”</em>“</span><span class="token punctuation">)</span><span class="token operator">+</span> final_digits<span class="token punctuation">;</span><span class="token punctuation">}</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>^((?:[\d]+-)+)(\d+)$<span class="token punctuation">“</span></span><span class="token attr-name">replacer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>return cardNumberReplacer(arguments)<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>cc<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Credit Card<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>text<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>cc<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>cc<span class="token punctuation">“</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>1234-5678-9012-3456<span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This displays as <code><strong><strong>-</strong></strong>-****-3456</code>, showing only the last group of digits.</p><h2 id="combining-attributes" tabindex="-1"><a class="header-anchor" href="#combining-attributes" aria-hidden="true">#</a> Combining attributes</h2><p>You can combine these attributes for sophisticated obfuscation patterns:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>\d{4}$<span class="token punctuation">“</span></span><span class="token attr-name">character</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>•<span class="token punctuation">“</span></span><span class="token attr-name">maxlength</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>16<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>card<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Credit Card<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>text<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>card<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>card<span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This keeps the last 4 digits visible, uses bullets for obfuscation, and limits the display to 16 characters total.</p><h2 id="event-handling" tabindex="-1"><a class="header-anchor" href="#event-handling" aria-hidden="true">#</a> Event handling</h2><p>The component dispatches custom events when values are hidden or revealed:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> obfuscator <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>obfuscator<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator:hide”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Field obfuscated:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>field<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>obfuscator<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator:reveal”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Field revealed:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>field<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>You can access both the visible field and the hidden field through <code>event.detail.field</code> and <code>event.detail.hidden</code> respectively.</p><h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works" aria-hidden="true">#</a> How it works</h2><p>The component creates a hidden <code>input</code> field to store the actual value for form submission. When the visible field loses focus, it:</p><ol><li>Copies the current value to the hidden field</li><li>Applies your obfuscation rules to create the display value</li><li>Updates the visible field with the obfuscated value</li><li>Dispatches the <code>form-obfuscator:hide</code> event</li></ol><p>When the field gains focus, it:</p><ol><li>Restores the real value from the hidden field</li><li>Updates the visible field</li><li>Dispatches the <code>form-obfuscator:reveal</code> event</li></ol><p>The source order ensures the hidden field is the one that gets submitted with the form.</p><h2 id="progressive-enhancement" tabindex="-1"><a class="header-anchor" href="#progressive-enhancement" aria-hidden="true">#</a> Progressive enhancement</h2><p>The component makes no assumptions about your markup—it works with any text-style <code>input</code> element. If JavaScript fails to load, the field behaves like a normal <code>input</code>, which is exactly what you want. Users can still enter and submit values; they just won’t get the obfuscation behavior.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I’ve created <a href="https://aarongustafson.github.io/form-obfuscator/demo/">a comprehensive demo page showing the various configuration options</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-obfuscator/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>Check out the full project over on <a href="https://github.com/aarongustafson/form-obfuscator">GitHub</a> or install via <code>npm</code>:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-obfuscator</code></pre><p>Import and use:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">”@aarongustafson/form-obfuscator&quot;</span><span class="token punctuation">;</span></code></pre><p>No dependencies, just a straightforward way to add field obfuscation to your forms.</p>]]></content><amg:twitter><![CDATA[Need to obfuscate form field values when they’re not being edited? Here’s a web component for that.]]></amg:twitter><amg:summary><![CDATA[There’s no standard way to make a field readable while editing but obfuscated at rest. The <code>form-obfuscator</code> web component fills that gap, giving you control over how sensitive data appears when fields aren't focused.]]></amg:summary><summary type="html"><![CDATA[<p>There’s no standard way to make a field readable while editing but obfuscated at rest. The <code>form-obfuscator</code> web component fills that gap, giving you control over how sensitive data appears when fields aren't focused.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><category term="security" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/creating-a-more-accessible-web-with-aria-notify/</id><title type="html"><![CDATA[🔗 Creating a more accessible web with ARIA Notify]]></title><link href="https://www.aaron-gustafson.com/notebook/links/creating-a-more-accessible-web-with-aria-notify/" rel="alternate" type="text/html" /><link href="https://blogs.windows.com/msedgedev/2025/05/05/creating-a-more-accessible-web-with-aria-notify/" rel="related" type="text/html" /><published>2025-11-26T18:40:45Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I just saw this very exciting announcement on the Edge Dev Blog:</p><blockquote><p>ARIA Notify is an ergonomic and predictable way to tell assistive technologies (ATs), such as screen readers, exactly what to announce to users and when.</p><p>In its simplest form, developers can call the ariaNotify() method with the text to be announced to the user.</p></blockquote><p>Here’s what it looks like:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token comment">// Dispatch a normal priority notification</span>document<span class="token punctuation">.</span><span class="token function">ariaNotify</span><span class="token punctuation">(</span><span class="token string">“Background task completed”</span><span class="token punctuation">,</span><span class="token punctuation">{</span><span class="token literal-property property">priority</span><span class="token operator">:</span><span class="token string">“normal”</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>I’m particularly excited by this because of how much it simplifies the update process for engineers. Previously they needed to manage upates to an <code>aria-live</code> DOM node with the appropriate announcement level and hope for the best. This approach was plagued with issues ranging from lag — because, DOM manipulation — to confusion between whether “polite” or “assertive” was the right choice.</p><p>The ARIA Notify proposal is clear, concise, and far more likely to get used and — more importantly — used properly.</p><p>It’s currently in Origin Trial. Please give your feedback so we can get this into every browser sooner rather than later.</p>]]></content><amg:twitter><![CDATA[ARIA Notify looks like it could solve a lot of problems with screen reader announcements by simplifying the process.]]></amg:twitter><category term="accessibility" /><category term="JavaScript" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/default-isn-t-design/</id><title type="html"><![CDATA[🔗 Default Isn’t Design]]></title><link href="https://www.aaron-gustafson.com/notebook/links/default-isn-t-design/" rel="alternate" type="text/html" /><link href="https://eisenbergeffect.medium.com/default-isnt-design-24df33272abb" rel="related" type="text/html" /><published>2025-10-16T21:22:41Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<blockquote><p>When one approach becomes “how things are done,” we unconsciously defend it even when standards would give us a healthier, more interoperable ecosystem. Psychologists call this reflex System Justification. Naming it helps us steer toward a standards-first future without turning the discussion into a framework war.</p></blockquote><p>This whole piece is an excellent discussion about how tools can become an identity and why that’s a bad thing.</p>]]></content><amg:twitter><![CDATA[👍🏻 “When one approach becomes ‘how things are done,’ we unconsciously defend it even when standards would give us a healthier, more interoperable ecosystem.”]]></amg:twitter><category term="web standards" /><category term="JavaScript" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/passing-your-css-theme-to-canvas/</id><title type="html"><![CDATA[✍🏻 Passing Your CSS Theme to `canvas`]]></title><link href="https://www.aaron-gustafson.com/notebook/passing-your-css-theme-to-canvas/" rel="alternate" type="text/html" /><published>2025-05-01T21:49:27Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>While working on a recent project I noticed an issue with a <code>canvas</code>-based audio visualization when I toggled between light and dark modes. When I’d originally set it up I was browsing in dark mode and the light visualization stroke showed up perfectly on the dark background, but it was invisible when viewed using the light theme (which I’d neglected to test). I searched around, but didn’t find any articles on easy ways to make <code>canvas</code> respond nicely to user preferences, so I thought I’d share (in brief) how I solved it.</p><h2 id="the-css-setup" tabindex="-1"><a class="header-anchor" href="#the-css-setup" aria-hidden="true">#</a> The CSS Setup</h2><p>The theming of this particular project uses <a href="https://developer.mozilla.org/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties">CSS custom properties</a>. For simplicty I’m going to set up two named colors and then use two theme-specific custom properties to apply them in the default light theme and the dark theme:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token selector">:root</span><span class="token punctuation">{</span><span class="token property">–color-dark</span><span class="token punctuation">:</span> #222<span class="token punctuation">;</span><span class="token property">–color-light</span><span class="token punctuation">:</span><span class="token function">rgba</span><span class="token punctuation">(</span>255<span class="token punctuation">,</span> 255<span class="token punctuation">,</span> 255<span class="token punctuation">,</span> 0.5<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">–color-background</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-light<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">–color-foreground</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-dark<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token atrule"><span class="token rule">@media</span><span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> dark<span class="token punctuation">)</span></span><span class="token punctuation">{</span><span class="token selector">:root</span><span class="token punctuation">{</span><span class="token property">–color-background</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-dark<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">–color-foreground</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-light<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span></code></pre><h2 id="applying-the-theme-to-canvas" tabindex="-1"><a class="header-anchor" href="#applying-the-theme-to-canvas" aria-hidden="true">#</a> Applying the Theme to Canvas</h2><p>To get the theme into my <code>canvas</code>-related code, I set up a <code>theme</code> object to hold the values:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">const</span> theme <span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre><p>Next, I wrote a function to pull in the theme colors using <a href="https://developer.mozilla.org/docs/Web/API/Window/getComputedStyle"><code>window.getComputedStyle()</code></a>. After defining the function, I call it immediately to populate the <code>theme</code> object:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">function</span><span class="token function">importTheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>theme<span class="token punctuation">.</span>foreground <span class="token operator">=</span>window<span class="token punctuation">.</span><span class="token function">getComputedStyle</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>documentElement<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getPropertyValue</span><span class="token punctuation">(</span><span class="token string">“–color-foreground”</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">||</span><span class="token string">“black”</span><span class="token punctuation">;</span>theme<span class="token punctuation">.</span>background <span class="token operator">=</span>window<span class="token punctuation">.</span><span class="token function">getComputedStyle</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>documentElement<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getPropertyValue</span><span class="token punctuation">(</span><span class="token string">“–color-background”</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">||</span><span class="token string">“white”</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token function">importTheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>I set this up with just two theme colors, but you can import as many (or few) as you like. Be sure to set a sensible default or fallback for each color though, just in case your theme’s custom property names change.</p><p>With this in place, I can set my <code>canvas</code> animation’s colors by referencing them from the <code>theme</code> object. For example:</p><pre class="language-js" tabindex="0"><code class="language-js">context<span class="token punctuation">.</span>fillStyle <span class="token operator">=</span> theme<span class="token punctuation">.</span>foreground<span class="token punctuation">;</span></code></pre><h2 id="keeping-things-in-sync" tabindex="-1"><a class="header-anchor" href="#keeping-things-in-sync" aria-hidden="true">#</a> Keeping Things in Sync</h2><p>The final bit of magic comes when you add an event listener to a <code>MediaQueryList</code>:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">const</span> mediaQuery <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">“(prefers-color-scheme: dark)”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>mediaQuery<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“change”</span><span class="token punctuation">,</span> importTheme<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Here I’ve used <code>matchMedia()</code> to get a <code>MediaQueryList</code> object. Typically we use the <code>matches</code> property of this object to establish whether the media query currently matches or not. A lesser-known option, however, is that you can attach an event listener to it that will be triggered whenever the query’s status changes. So cool! With this in place, the <code>canvas</code> contents will update whenever the user’s theme changes. <a href="#fig-2025-05-01-01">Here’s an example of that</a>:</p><figure id="fig-2025-05-01-01" class="media-container"><p><a href="https://www.youtube.com/watch?v=pALIuO5uHUA">https://www.youtube.com/watch?v=pALIuO5uHUA</a></p><figcaption><p>This video demonstrates how a canvas element rendering a dark sine wave against a light background can miraculously transform into a light sine wave against a dark background using CSS custom properties and a bit of JavaScript.</p></figcaption></figure><h1 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h1><p>I put together <a href="https://codepen.io/aarongustafson/pen/LEEQyqg">a quick demo of this</a> in a fork of <a href="https://codepen.io/alvinshaw/pen/mdEKggg">Alvin Shaw’s Canvas Sine Wave Experiment</a>:</p><figure id="fig-2025-05-01-02" class="media-container"><iframe class="codepen" height="331" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/LEEQyqg?height=331&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/aarongustafson/pen/LEEQyqg" target="_blank" rel="noopener">See the Pen</a></p></iframe></figure><hr><p>Hopefully this is helpful to someone out there. Happy theming!</p>]]></content><amg:twitter><![CDATA[Need to pipe your CSS theme into a `canvas` element? Here’s how I did it.]]></amg:twitter><amg:summary><![CDATA[While working on a recent project I noticed an issue with a <code>canvas</code>-based audio visualization when I toggled between light and dark modes. I couldn’t find any articles on to make <code>canvas</code> respond nicely to user preferences, so I thought I’d share (in brief) how I solved it.]]></amg:summary><summary type="html"><![CDATA[<p>While working on a recent project I noticed an issue with a <code>canvas</code>-based audio visualization when I toggled between light and dark modes. I couldn’t find any articles on to make <code>canvas</code> respond nicely to user preferences, so I thought I’d share (in brief) how I solved it.</p>]]></summary><category term="accessibility" /><category term="animation" /><category term="CSS" /><category term="design" /><category term="JavaScript" /></entry><entry><id>https://www.aaron-gustafson.com/publications/books/learning-web-design-sixth-edition/</id><title type="html"><![CDATA[📗 Learning Web Design]]></title><link href="https://www.oreilly.com/library/view/learning-web-design/9781098137670/" rel="alternate" type="text/html" /><published>2025-05-01T00:00:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Do you want to build web pages but have no prior experience? This friendly guide is the perfect place to start. You’ll begin at square one, learning how the web and web pages work, and then steadily build from there. By the end of the book, you’ll have the skills to create a simple site with multicolumn pages that adapt for mobile devices.</p>]]></content><category term="accessibility" /><category term="CSS" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="responsive web design" /><category term="web design" /><category term="web development" /><category term="web standards" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/web-components-are-not-the-future-they-re-the-present/</id><title type="html"><![CDATA[🔗 Web Components Are Not the Future — They’re the Present]]></title><link href="https://www.aaron-gustafson.com/notebook/links/web-components-are-not-the-future-they-re-the-present/" rel="alternate" type="text/html" /><link href="https://www.abeautifulsite.net/posts/web-components-are-not-the-future-they-re-the-present/" rel="related" type="text/html" /><published>2024-09-27T23:46:46Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I really appreciated Cory LaViska’s take on #WebComponents here. Especially this bit:</p><blockquote><p>You know what framework I want to use? I want a framework that aligns with the platform, not one that replaces it. I want a framework that values incremental innovation over user lock-in. I want a framework that says it’s OK to break things if it means making the Web a better place for everyone. Yes, that comes at a cost, but almost every good investment does, and I would argue that cost will be less expensive than learning a new framework and rebuilding buttons for the umpteenth time.</p></blockquote>]]></content><amg:twitter><![CDATA[I really appreciated Cory LaViska’s take on #WebComponents here.]]></amg:twitter><category term="web components" /><category term="web standards" /><category term="JavaScript" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-for-conditional-dependent-fields/</id><title type="html"><![CDATA[✍🏻 A Web Component for Conditional Dependent Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-for-conditional-dependent-fields/" rel="alternate" type="text/html" /><published>2024-08-14T03:26:03Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>A few weeks back I released <a href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/">a web component to enable you to add requirement rules to checkbox groups</a>. Continuing in the form utility space, I’ve created a new web component that allows you to make fields required based on the values of other fields: <code>form-required-if</code>.</p><p>The <code>form-required-if</code> web component, which is based on <a href="https://github.com/easy-designs/jquery.easy-dependent-required-fields.js">a jQuery plugin I’d written in 2012</a>, looks like this:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email=<em><span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Required if there’s an email value<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>depends-on-email<span class="token punctuation">&quot;</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>You wrap any field (and its <code>label</code>) in the component and then declare the conditions under which it should be required in the <code>conditions</code> attribute.</p><h2 id="defining-the-requirement-conditions" tabindex="-1"><a class="header-anchor" href="#defining-the-requirement-conditions" aria-hidden="true">#</a> Defining the requirement conditions</h2><p>Each condition is a key/value pair where the key aligns to the <code>name</code> of the field you need to observe and the value is the value that could trigger the dependency. If any value should trigger the dependency, you use an asterisk (</em>) as the value. In the example above, the field will become required when any value is assigned to the field matching <code>[name=&quot;email&quot;]</code>.</p><p>This <code>conditions</code> attribute can be populated with as many or as few dependencies as make sense for your use case. Multiple conditions are separated by double vertical pipes (<code>||</code> a.k.a. <em>or</em>) as in this example:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email=<em>||test=3<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Depends on email or test field<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>depends-on-email-or-test<span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>Here the field depends on one of the following conditions being true:</p><ol><li>the field matching <code>[name=&quot;email&quot;]</code> has a value, <em>or</em></li><li>the field matching <code>[name=&quot;test&quot;]</code> has a value of “3”</li></ol><p>If the field you reference doesn’t exist, no errors will be thrown, it will just quietly exit.</p><h2 id="visually-indicating-a-field-is-required" tabindex="-1"><a class="header-anchor" href="#visually-indicating-a-field-is-required" aria-hidden="true">#</a> Visually indicating a field is required</h2><p>If you typically use an asterisk or similar to indicate a field is required, this web component can support that through one or both of the following attributes:</p><ul><li><code>indicator</code> - This attribute is where you define the indicator itself. It could be something as simple as a string (e.g., <em>), or even full-blown HTML.</li><li><code>indicator-placement</code> - As you can probably guess, this attribute is used to set the position of the indicator. If you want it at the start of the label text, you give it the value “before.” If you want it after the text, you use “after” or don’t use the attribute at all. Indicators will be placed after the label text by default.</li></ul><p>Here’s an example with a custom indicator that is HTML:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>email=</em><span class="token punctuation">”</span></span><span class="token attr-name">indicator</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>&lt;b&gt;</em>&lt;/b&gt;<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Depends on email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>dep2<span class="token punctuation">&quot;</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>If you don’t include markup in your indicator, it will be automatically wrapped in <code>span</code> when injected into the DOM. The <code>hidden</code> and <code>aria-hidden</code> attributes are used to toggle its visibility, relative to the requirement state of the field.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I put together <a href="https://aarongustafson.github.io/form-required-if/demo/">a comprehensive demo of the web component</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-required-if/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab It</h2><p>You can view the entire project (and suggest enhancements) over on <a href="https://github.com/aarongustafson/form-required-if">the <code>form-required-if</code> component’s Github repo</a>.</p>]]></content><amg:twitter><![CDATA[Sometimes you need to require a form field only if another field is filled in or has a specific value. Here’s a web component to enable that.]]></amg:twitter><amg:summary><![CDATA[Sometimes you need to require a value in one form field only if another field is filled in or if that field has a specific value. I ported an old jQuery plugin of mine that made this work to a web component you can use easily right now.]]></amg:summary><summary type="html"><![CDATA[<p>Sometimes you need to require a value in one form field only if another field is filled in or if that field has a specific value. I ported an old jQuery plugin of mine that made this work to a web component you can use easily right now.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/don-t-use-js-for-that-moving-features-to-css-and-html-by-kilian-valkhof/</id><title type="html"><![CDATA[🔗 Don't Use JS for That: Moving Features to CSS and HTML by Kilian Valkhof]]></title><link href="https://www.aaron-gustafson.com/notebook/links/don-t-use-js-for-that-moving-features-to-css-and-html-by-kilian-valkhof/" rel="alternate" type="text/html" /><link href="https://www.youtube.com/watch?v=IP_rtWEMR0o" rel="related" type="text/html" /><published>2024-07-29T17:22:19Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This is a fantastic run-through of HTML and CSS features that help reduce our dependence on JavaScript (and improve #accessibility). Great work Kilian!</p>]]></content><amg:twitter><![CDATA[This is a fantastic run-through of #HTML and #CSS features that help reduce our dependence on #JavaScript (and improve #accessibility).]]></amg:twitter><category term="HTML" /><category term="CSS" /><category term="JavaScript" /><category term="progressive enhancement" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/on-crowdstrike-dependencies-and-building-robust-products-on-the-web/</id><title type="html"><![CDATA[✍🏻 On CrowdStrike, dependencies, and building robust products on the web]]></title><link href="https://www.aaron-gustafson.com/notebook/on-crowdstrike-dependencies-and-building-robust-products-on-the-web/" rel="alternate" type="text/html" /><published>2024-07-25T17:20:26Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I have no opinion on CrowdStrike as a company or service. I’ve never used their products. In fact, prior to <a href="https://wikipedia.org/wiki/2024_CrowdStrike_incident">the incident last week</a>, I had only a passing familiarity with their name — likely from headlines in the tech press I’d scrolled past at some point in time. I now have a vague understanding of what they do, but that’s only based on what I learned about <a href="https://www.crowdstrike.com/wp-content/uploads/2024/07/CrowdStrike-PIR-Executive-Summary.pdf">the cause of the incident</a>. In reflecting on this unfortunate incident, I can’t help but think of the lesson it holds for web designers and developers.</p><p>The incident was caused when a bug in CrowdStrike’s code made it out into production. The results were catastrophic: It caused roughly 8.5 million servers to crash. Hospitals weren’t able to serve the people that needed them. Airline passengers were stranded. People couldn’t access their money in banks. Folks in distress could not get the emergency services they needed. On top of that, the financial fallout is estimated to be somewhere around US$10 billion.</p><p>Bugs happen. I’d hate to be the person who was responsible for that particular bug (or the people in the quality assurance team that should have caught it), but the reality is that none of us who write code write <em>perfect</em> code. We all make mistakes and sometimes those mistakes make it into production. Other times, the code we write works perfectly during development and testing, but causes an unexpected issue in production. Sometimes only in very specific “edge case” circumstances that we didn’t have the foresight to consider.</p><p>Which brings me to the lesson I took away from the CrowdStrike incident: minimize the impact dependencies can have on your customers’ ability to complete critical tasks. In other words, <a href="https://www.smashingmagazine.com/2016/05/developing-dependency-awareness/">develop dependency awareness</a>.</p><p>The web is a hostile operating environment. Sometimes network connections are slow to resolve or time out completely, which may cause issues with your JavaScript, may result in broken images or videos, or could make it so your user never receives your CSS. Sometimes 3rd party scripts ship bugs that can hose your site completely. Sometimes a customer’s browser plugin can wreak havoc on your site by adjusting markup or injecting code. If any (or all) of those things were to happen, could your customers still accomplish their key tasks? Could they even understand your site at all?</p><p>This is why it’s so critical to start with a fully-functional website that relies only on semantic, accessible HTML and regular ol’ links and form submissions. They aren’t sexy, but they’re solid. Then <em>progressively enhance</em> that experience to improve things when the CSS is downloaded. And improve some more when your JavaScript executes properly. Build an awareness for the kinds of dependencies you have in your code so you can ensure there is always a fallback.</p><p>When I think about building robust websites like this, I often think of the Chrysler Imperial. The 1964-1966 model is one of the few cars that has been outright banned from entering demolition derby events. It is just too well built. It just takes the hits and keeps on driving. We should aspire to that kind of resilience in the websites we build.</p><p>Bugs happen. Can your site withstand them? Or will you let the failure of a single dependency (a.k.a., site fragility) ruin your customers’ day?</p><p><a href="https://www.youtube.com/watch?v=-9GGDOUDLhc&amp;start=7&amp;end=17">https://www.youtube.com/watch?v=-9GGDOUDLhc&amp;start=7&amp;end=17</a></p>]]></content><amg:twitter><![CDATA[The #CrowdStrike meltdown last week should be a cautionary tale for #WebDesigners and #WebDevleopers.]]></amg:twitter><amg:summary><![CDATA[The CrowdStrike meltdown last week should be a cautionary tale for web designers and web developers.]]></amg:summary><summary type="html"><![CDATA[<p>The CrowdStrike meltdown last week should be a cautionary tale for web designers and web developers.</p>]]></summary><category term="progressive enhancement" /><category term="hazards" /><category term="CSS" /><category term="JavaScript" /><category term="web design" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/</id><title type="html"><![CDATA[✍🏻 Requirement Rules for Checkboxes]]></title><link href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/" rel="alternate" type="text/html" /><published>2024-07-05T21:08:34Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>HTML checkboxes debuted as <a href="https://datatracker.ietf.org/doc/html/rfc1866#section-8.1.2.3">part of HTML 2.0 in 1995</a>. Our ability to mark an individual checkbox as being required became part of the HTML5 spec that published in 2014. A decade later, we can still only make checkboxes required on a case-by-case basis. To overcome this limitation, I had created <a href="https://github.com/easy-designs/easy-checkbox-required.js">a jQuery plugin that allowed me to indicate that a user should choose a specific number of items from within a checkbox group</a>. Yesterday I turned that plugin into a web component: <a href="https://github.com/aarongustafson/form-required-checkboxes"><code>form-required-checkboxes</code></a>.</p><h2 id="markup-assumptions" tabindex="-1"><a class="header-anchor" href="#markup-assumptions" aria-hidden="true">#</a> Markup Assumptions</h2><p>Before I tuck into the details, I’ll start by saying that the web component begins with the assumption that you are following best practices with respect to form markup:</p><ul><li>Your checkbox group should be in a <code>fieldset</code> with a <code>legend</code></li><li>All of the checkbox elements must have the same <code>name</code> (e.g., “foo[]”).</li></ul><p>In other words, they should look something like this:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Group 1 label<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>checkbox<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>foo[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>1<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span>First item<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>checkbox<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>foo[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span>Second item<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- options continue --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span></code></pre><p>To use the web component, you wrap the group in a <code>form-required-checkboxes</code> element and then include the JavaScript to initialize it.</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-checkboxes</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Group 1 label<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- etc. --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-checkboxes</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- at the end of your document --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/js/web-components/form-required-checkboxes.js<span class="token punctuation">”</span></span><span class="token attr-name">async</span><span class="token punctuation">&gt;</span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span></code></pre><p>If you’re following right along, there’s an error waiting for you in the <code>console</code> — we need to set the requirement rules.</p><h2 id="the-api" tabindex="-1"><a class="header-anchor" href="#the-api" aria-hidden="true">#</a> The API</h2><p>The <code>form-required-checkboxes</code> element requires at least one attribute to function, but using some of the others you can more fully customize the experience for users:</p><ul><li><code>required</code> - Represents the range of required values. You can set this up in one of three ways depending on your needs:<ul><li>Single number (e.g., 3) requires exactly that number of choices.</li><li>Range (e.g., 3-5) requires a minimum of the first number and a max of the second number be chosen.</li><li>Max (e.g., 0-3) requires a minimum of zero and a max of the second number to be chosen.</li></ul></li><li><code>notice</code> (optional) - This is a string description that explains details of the required value in plain language. If you don’t supply one, the component will create one for you based on the current language (if supported). This description will be added as a <code>small</code> element within the component (as a sibling to the <code>fieldset</code>).</li><li><code>error</code> (optional) - This is a string validation error you’d like to be shown when the validation criteria is not met. If not provided, an appropriate error message will be generated based on the current language (if supported).</li><li><code>lang</code> (optional) - Language code for localized messages (e.g., “en,” “es,” “fr,” “de”). Falls back to the nearest ancestor’s <code>lang</code> attribute or the document language.</li></ul><h2 id="localization" tabindex="-1"><a class="header-anchor" href="#localization" aria-hidden="true">#</a> Localization</h2><p>The component now includes built-in translations for 16 languages: English, Chinese (Mandarin), Hindi, Spanish, French, Arabic, Bengali, Portuguese, Russian, Japanese, German, Punjabi, Javanese, Korean, Vietnamese, and Italian. Messages are automatically generated based on the <code>lang</code> attribute.</p><p>You can use it like this:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-checkboxes</span><span class="token attr-name">required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>3<span class="token punctuation">”</span></span><span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>es<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Opciones<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Will display: “Elija 3 de la lista” --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-checkboxes</span><span class="token punctuation">&gt;</span></span></code></pre><p>The component automatically detects the language from the <code>lang</code> attribute on the element itself, the nearest ancestor element, or the document’s <code>lang</code> attribute, falling back to English if none is found.</p><p>You can also register custom translations or override existing ones using the <code>FormRequiredCheckboxesElement.registerTranslations()</code> static method. Regional language codes (e.g., <code>en-US</code>, <code>es-MX</code>) automatically fall back to their base language.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I put together <a href="https://aarongustafson.github.io/form-required-checkboxes/demo/">a comprehensive demo of the web component</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-required-checkboxes/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab It</h2><p>You can view the entire project (and suggest enhancements) over on <a href="https://github.com/aarongustafson/form-required-checkboxes">the component’s Github repo</a>.</p>]]></content><amg:twitter><![CDATA[Currently, we can only make checkboxes required or not, individually. In some cases you need to be able to set a specific number of checkboxes that need to be checked. My `form-required-checkboxes` web component enables that.]]></amg:twitter><amg:summary><![CDATA[Currently, we can only make checkboxes required or not, individually. In some cases you need to be able to set a specific number of checkboxes that need to be checked. My <code>form-required-checkboxes</code> web component enables that.]]></amg:summary><summary type="html"><![CDATA[<p>Currently, we can only make checkboxes required or not, individually. In some cases you need to be able to set a specific number of checkboxes that need to be checked. My <code>form-required-checkboxes</code> web component enables that.</p>]]></summary><category term="accessibility" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/todo-app-with-no-client-side-javascript-using-lazarv-react-server/</id><title type="html"><![CDATA[🔗 Todo app with no client-side JavaScript using @lazarv/react-server]]></title><link href="https://www.aaron-gustafson.com/notebook/links/todo-app-with-no-client-side-javascript-using-lazarv-react-server/" rel="alternate" type="text/html" /><link href="https://dev.to/lazarv/todo-app-with-no-client-side-javascript-using-lazarvreact-server-23ig" rel="related" type="text/html" /><published>2024-05-29T16:58:58Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I love straightforward examples, like this one, of how to build progressively enhanced experiences in frameworks like React.</p><p>Step 2: Add in some HTML web components!</p>]]></content><amg:twitter><![CDATA[I love straightforward examples, like this one, of how to build progressively enhanced experiences in frameworks like React.]]></amg:twitter><category term="JavaScript" /><category term="progressive enhancement" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/purepwa-a-radical-u-turn-in-web-development/</id><title type="html"><![CDATA[🔗 PurePWA — A Radical U-Turn in Web Development]]></title><link href="https://www.aaron-gustafson.com/notebook/links/purepwa-a-radical-u-turn-in-web-development/" rel="alternate" type="text/html" /><link href="https://medium.com/@neerventure/purepwa-a-radical-u-turn-in-web-development-a386c0dc092e" rel="related" type="text/html" /><published>2024-02-02T18:38:09Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I love experiments that showcase the power of the web platform and this is no exception: a pure web standards-powered Progressive Web App (PWA) replete with straightforward web components, view transitions, and more.</p>]]></content><amg:twitter><![CDATA[I love experiments that showcase the power of the web platform and this is no exception: a pure #WebStandards-powered #ProgressiveWebApp]]></amg:twitter><category term="progressive web apps" /><category term="HTML" /><category term="web components" /><category term="CSS" /><category term="JavaScript" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://miro.medium.com/v2/resize:fit:1200/1*ZEDw2ropDtOTGm_RUIMJlg.png" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/css-media-query-for-scripting-support/</id><title type="html"><![CDATA[🔗 CSS Media Query for Scripting Support]]></title><link href="https://www.aaron-gustafson.com/notebook/links/css-media-query-for-scripting-support/" rel="alternate" type="text/html" /><link href="https://blog.stephaniestimac.com/posts/2023/12/css-media-query-scripting/" rel="related" type="text/html" /><published>2024-01-05T23:12:57Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>The new <code>scripting</code> media query looks like a powerful addition to the progressive enhancement toolbox.</p>]]></content><amg:twitter><![CDATA[The new `scripting` media query looks like a powerful addition to the progressive enhancement toolbox.]]></amg:twitter><category term="CSS" /><category term="JavaScript" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.stephaniestimac.com" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/an-attempted-taxonomy-of-web-components/</id><title type="html"><![CDATA[🔗 An Attempted Taxonomy of Web Components]]></title><link href="https://www.aaron-gustafson.com/notebook/links/an-attempted-taxonomy-of-web-components/" rel="alternate" type="text/html" /><link href="https://www.zachleat.com/web/a-taxonomy-of-web-component-types/" rel="related" type="text/html" /><published>2024-01-05T23:10:55Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I really appreciate Zach putting this together as it helped me sort through some ideas I had in my own head. It also kinda makes me want to update <a href="https://github.com/orgs/easy-designs/repositories?q=&amp;type=public&amp;language=javascript&amp;sort=">a bunch of my old JavaScript components</a> to remake them as lightweight HTML Web Components.</p>]]></content><amg:twitter><![CDATA[A great overview of different approaches to Web Components, when you’d want to use them, and more.]]></amg:twitter><category term="web components" /><category term="HTML" /><category term="JavaScript" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.zachleat.com%2Fopengraph%2Fweb%2Fa-taxonomy-of-web-component-types%2F/opengraph/_x202401_5/" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/widgets/</id><title type="html"><![CDATA[✍🏻 Widgets!]]></title><link href="https://www.aaron-gustafson.com/notebook/widgets/" rel="alternate" type="text/html" /><published>2023-10-09T22:38:54Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>It was a long time coming, but I finally had a chance to put the work I did on <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/tree/main/PWAWidgets">a widgets proposal for PWAs</a> into practice on my own site. I’m pretty excited about it!</p><h2 id="where-it-all-started" tabindex="-1"><a class="header-anchor" href="#where-it-all-started" aria-hidden="true">#</a> Where it all started</h2><p>I had <a href="https://web.archive.org/web/20200929174844/https://discourse.wicg.io/t/noodling-on-an-idea-projections-for-web-apps/3900">the original idea for “projections”</a> way back in 2019. Inspired by <a href="https://en.wikipedia.org/wiki/Dashboard_(macOS)#Widget_functions_and_capabilities">OS X’s Dashboard Widgets</a> and <a href="https://en.wikipedia.org/wiki/Adobe_AIR">Adobe AIR</a>, I’d begun to wonder if it might be possible to <em>project</em> a component from a website into those kinds of surfaces. Rather than building a bespoke widget that connected to an API, I thought it made sense to leverage an installed PWA to manage those “projections.” I shared the idea at TPAC that year and got some interest from a broad range of folks, but didn’t have much time to work on the details until a few years later.</p><p>In the intervening time, I kept working through the concept in my head. I mean in an ideal world, the widget would just be a responsive web page, right? But if that were the case, what happens when every widget loads the entirety of React to render their stock ticker? That seemed like a performance nightmare.</p><p>In my gut, I felt like the right way to build things would be to have a standard library of widget templates and to enable devs to flow data into them via a Service Worker. Alex Russell suggested I model the APIs on how Notifications are handled (since they serve a similar function) and I was off to the races.</p><p>I drafted <a href="https://github.com/aarongustafson/pwa-widgets">a substantial proposal for my vision of how PWA widgets should work</a>. Key aspects included:</p><ul><li>A declarative way to define and configure a widget from within the Web App Manifest;</li><li>A progressively enhanced pathway for devs to design a widget that adapts to its host environment, from using predefined templates to using custom templates to full-blown web-based widgets (with rendering akin to an <code>iframe</code>);</li><li>A collection of recommended stock templates that implementors should offer to support most widget types;</li><li>Extensibility to support custom templates using any of a variety of templating languages; and</li><li>A complete suite of tools for managing widgets and any associated business logic within a Service Worker.</li></ul><h2 id="widgets-became-a-reality" tabindex="-1"><a class="header-anchor" href="#widgets-became-a-reality" aria-hidden="true">#</a> Widgets became a reality</h2><p>After continuing to gently push on this idea with colleagues across Microsoft (and beyond), I discovered that the Windows 11 team was looking to open up the new Widget Dashboard to third-party applications. I saw this as an opportunity to turn my idea into a reality. After working my way into the conversation, I made a solid case for why PWAs needed to be a part of that story and… it worked! (It no doubt helped that companies including Meta, Twitter, and Hulu were all invested in PWA as a means of delivering apps for Windows.)</p><p>While the timeline for implementation didn’t allow us to tackle the entirety of my proposal, we did carve out the pieces that made for a compelling MVP. This allowed us to show what’s possible, see how folks use it, and plan for future investment in the space.</p><p>Sadly, it meant tabling two features I really loved:</p><ul><li><strong>Stock/predefined templates</strong>. A library of lightly theme-able, consistent, cross-platform templates based on common data structures (e.g., RSS/Atom, iCal) would make it incredibly simple for devs to build a widget. If implemented well, devs might not even need to write a single line of business logic in their Service Worker as the browser could pick up all of the configuration details from the Manifest.</li><li><strong>Configurable widget instances.</strong> Instead of singleton widgets, these would allow you to define a single widget type and replicate it for different use cases. For example, a widget to follow a social media user’s profile could be defined once and the individual instances could be configured with the specific account to be followed.</li></ul><p>I’m sincerely hopeful these two features eventually make their way to us as I think they truly unlock the power of the widget platform. Perhaps, with enough uptake on the current implementation, we can revisit these in the not-too-distant future.</p><hr><p>To test things out, I decided to build two widgets for this site:</p><ol><li>Latest posts</li><li>Latest links</li></ol><p>Both are largely the same in terms of their setup: They display a list of linked titles from this site.</p><h2 id="designing-my-widget-templates" tabindex="-1"><a class="header-anchor" href="#designing-my-widget-templates" aria-hidden="true">#</a> Designing my widget templates</h2><p>Given that they were going to be largely identical, I made <a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/w/feed.ac.json">a single “feed” template for use in both widgets</a>. The templating tech I used is called <a href="https://adaptivecards.io">Adaptive Cards</a>, which is what Windows 11 uses for rendering.</p><p>Adaptive Card templates are relatively straightforward JSON:</p><pre class="language-json" tabindex="0"><code class="language-json">&amp;#<span class="token number">123</span>;<span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“AdaptiveCard”</span><span class="token punctuation">,</span><span class="token property">“$schema”</span><span class="token operator">:</span><span class="token string">“<a href="http://adaptivecards.io/schemas/adaptive-card.json">http://adaptivecards.io/schemas/adaptive-card.json</a>”</span><span class="token punctuation">,</span><span class="token property">“version”</span><span class="token operator">:</span><span class="token string">“1.6”</span><span class="token punctuation">,</span><span class="token property">“body”</span><span class="token operator">:</span> &amp;#<span class="token number">91</span>;&amp;#<span class="token number">123</span>;<span class="token property">“$data”</span><span class="token operator">:</span><span class="token string">“$&amp;#123;take(items,5)&amp;#125;”</span><span class="token punctuation">,</span><span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“Container”</span><span class="token punctuation">,</span><span class="token property">“items”</span><span class="token operator">:</span> &amp;#<span class="token number">91</span>;&amp;#<span class="token number">123</span>;<span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“TextBlock”</span><span class="token punctuation">,</span><span class="token property">“text”</span><span class="token operator">:</span><span class="token string">“&amp;#91;$&amp;#123;title&amp;#125;&amp;#93;($&amp;#123;url&amp;#125;)”</span><span class="token punctuation">,</span><span class="token property">“wrap”</span><span class="token operator">:</span><span class="token boolean">true</span><span class="token punctuation">,</span><span class="token property">“weight”</span><span class="token operator">:</span><span class="token string">“Bolder”</span><span class="token punctuation">,</span><span class="token property">“spacing”</span><span class="token operator">:</span><span class="token string">“Padding”</span><span class="token punctuation">,</span><span class="token property">“height”</span><span class="token operator">:</span><span class="token string">“stretch”</span>&amp;#<span class="token number">125</span>;&amp;#<span class="token number">93</span>;<span class="token punctuation">,</span><span class="token property">“height”</span><span class="token operator">:</span><span class="token string">“stretch”</span>&amp;#<span class="token number">125</span>;&amp;#<span class="token number">93</span>;<span class="token punctuation">,</span><span class="token property">“backgroundImage”</span><span class="token operator">:</span> &amp;#<span class="token number">123</span>;<span class="token property">“url”</span><span class="token operator">:</span><span class="token string">“<a href="https://www.aaron-gustafson.com/i/background-logo.png">https://www.aaron-gustafson.com/i/background-logo.png</a>”</span><span class="token punctuation">,</span><span class="token property">“verticalAlignment”</span><span class="token operator">:</span><span class="token string">“Bottom”</span><span class="token punctuation">,</span><span class="token property">“horizontalAlignment”</span><span class="token operator">:</span><span class="token string">“Center”</span>&amp;#<span class="token number">125</span>;&amp;#<span class="token number">125</span>;</code></pre><p>What this structure does is:</p><ol><li>Create a container into which I will place the content;</li><li>Extract the first five <code>items</code> from the data being fed into the template (more on that in a moment);</li><li>Loop through each <code>item</code> and<ul><li>create a text block,</li><li>populate its content with Markdown to generate a linked title (using the <code>title</code> and <code>url</code> keys from the <code>item</code> object)</li><li>Set some basic styles to make the text bold, separate the titles a little and make them grow to fill the container; then, finally</li></ul></li><li>Set a background on the widget.</li></ol><p>The way Adaptive Cards work is that they flow JSON data into a template and render that. The variable names in the template map directly to the incoming data structure, so are totally up to you to define. As these particular widgets are feed-driven and <a href="https://www.aaron-gustafson.com/feeds/">this site already supports JSONFeed</a>, I set up the widgets to flow the appropriate feed into each and used the keys that were already there. For reference, here’s a sample JSONFeed <code>item</code>:</p><pre class="language-json" tabindex="0"><code class="language-json"><span class="token punctuation">{</span><span class="token property">“id”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“title”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“summary”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“content_html”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“url”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“tags”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token property">“date_published”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">}</span></code></pre><p>If you want to tinker with Adaptive Cards and make your own, you can do so with <a href="https://adaptivecards.io/designer/">their Designer tool</a>.</p><h3 id="defining-the-widgets-in-the-manifest" tabindex="-1"><a class="header-anchor" href="#defining-the-widgets-in-the-manifest" aria-hidden="true">#</a> Defining the widgets in the Manifest</h3><p>With a basic template created, the next step was to set up the two widgets in my Manifest. As they both function largely the same, I’ll just focus on the definition for one of them.</p><p>First off, defining widgets in the Manifest is done via the <code>widgets</code> member, which is an array (much like <code>icons</code> and <code>shortcuts</code>). Each widget is represented as an object in that array. Here is the definition for the “latest posts” widget:</p><pre class="language-json" tabindex="0"><code class="language-json"><span class="token punctuation">{</span><span class="token property">“name”</span><span class="token operator">:</span><span class="token string">“Latest Posts”</span><span class="token punctuation">,</span><span class="token property">“short_name”</span><span class="token operator">:</span><span class="token string">“Posts”</span><span class="token punctuation">,</span><span class="token property">“tag”</span><span class="token operator">:</span><span class="token string">“feed-posts”</span><span class="token punctuation">,</span><span class="token property">“description”</span><span class="token operator">:</span><span class="token string">“The latest posts from Aaron Gustafson’s blog”</span><span class="token punctuation">,</span><span class="token property">“template”</span><span class="token operator">:</span><span class="token string">“feed”</span><span class="token punctuation">,</span><span class="token property">“ms_ac_template”</span><span class="token operator">:</span><span class="token string">“/w/feed.ac.json”</span><span class="token punctuation">,</span><span class="token property">“data”</span><span class="token operator">:</span><span class="token string">“/feeds/latest-posts.json”</span><span class="token punctuation">,</span><span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“application/json”</span><span class="token punctuation">,</span><span class="token property">“auth”</span><span class="token operator">:</span><span class="token boolean">false</span><span class="token punctuation">,</span><span class="token property">“update”</span><span class="token operator">:</span><span class="token number">21600</span><span class="token punctuation">,</span><span class="token property">“icons”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token punctuation">{</span><span class="token property">“src”</span><span class="token operator">:</span><span class="token string">“/i/icons/webicon-rss.png”</span><span class="token punctuation">,</span><span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“image/png”</span><span class="token punctuation">,</span><span class="token property">“sizes”</span><span class="token operator">:</span><span class="token string">“120x120”</span><span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token property">“screenshots”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token punctuation">{</span><span class="token property">“src”</span><span class="token operator">:</span><span class="token string">“/i/screenshots/widget-posts.png”</span><span class="token punctuation">,</span><span class="token property">“sizes”</span><span class="token operator">:</span><span class="token string">“387x387”</span><span class="token punctuation">,</span><span class="token property">“label”</span><span class="token operator">:</span><span class="token string">“The latest posts widget”</span><span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">}</span></code></pre><p>Breaking this down:</p><ol><li><code>name</code> and <code>short_name</code> act much like these keys in the root of the Manifest as well as in <code>shortcuts</code>: The <code>name</code> value is used as the name for the widget unless there’s not enough room, in which case <code>short_name</code> is used.</li><li>You can think of <code>tag</code> as analogous to <code>class</code> in HTML sense. It’s a way of labeling a widget so you can easily reference it later. Each widget instance will have a unique id created by the widget service, but that instance (or all instances, if the widget supports multiple instances) can be accessed via the <code>tag</code>. But more on that later.</li><li>The <code>description</code> key is used for marketing the widget within a host OS or digital storefront. It should accurately (and briefly) describe what the widget does.</li><li>The <code>template</code> key is not currently used in the Windows 11 implementation but refers to the expected standard library widget template provided by the system. As a template library is not currently available, the <code>ms_ac_template</code> value is used to provide a URL to get the custom Adaptive Card (hence “ac”) template. The “ms_” prefix is there because it’s expected that this would be a Microsoft-proprietary property. It follows <a href="https://www.w3.org/TR/appmanifest/#proprietary-extensions">the guidance for extending the Manifest</a>.</li><li>The <code>data</code> and <code>type</code> keys define the path to the data that should be fed into the template for rendering by the widget host and the MIME of the data format it’s in. The Windows 11 implementation currently only accepts JSON data, but the design of widgets is set up to allow for this to eventually extend to other standardized formats like RSS, iCal, vCard, and such.</li><li><code>update</code> is an optional configuration member allowing you to set how often you’d like the widget to update, in seconds. Developers currently need to add the logic for implementing this into their Service Worker, but this setup allows the configuration to remain independent of the JavaScript code, making it easier to maintain.</li><li>Finally, <code>icons</code> and <code>screenshots</code> allow us to define how the widget shows up in the widget host and how it is promoted for install.</li></ol><p>When someone installs my site as a PWA, the information about the available widgets gets ingested by the browser. The browser then determines, based on the provided values and its knowledge of the available widget service(s) on the device, which widgets should be offered. On Windows 11, this information is <a href="https://learn.microsoft.com/en-us/windows/apps/develop/widgets/implement-widget-provider-cs#update-the-package-manifest">routed into the AppXManifest that governs how apps are represented in Windows</a>. The Windows 11 widget service can then read in the details about the available widgets and offer them for users to install.</p><figure id="2023-10-09-01"><p><img src="https://www.aaron-gustafson.com/i/posts/2023-10-09/widgets-promotion.gif" alt=""></p><figcaption>An animated capture of Windows 11’s widget promotion surface, showing 2 widgets available from this site’s PWA.</figcaption></figure><h2 id="adding-widget-support-to-my-service-worker" tabindex="-1"><a class="header-anchor" href="#adding-widget-support-to-my-service-worker" aria-hidden="true">#</a> Adding widget support to my Service Worker</h2><p>As I mentioned earlier, all of the plumbing for widgets is done within a Service Worker and is modeled on the Notifications API. I’m not going to exhaustively detail how it all works, but I’ll give you enough detail to get you started.</p><p>First off, widgets are exposed via the <code>self.widgets</code> interface. Most importantly, this interface lets you access and update any instances of a widget connected to your PWA.</p><h3 id="installing-a-widget" tabindex="-1"><a class="header-anchor" href="#installing-a-widget" aria-hidden="true">#</a> Installing a widget</h3><p>When a user chooses to install a widget, that emits a “widgetinstall” event in your Service Worker. You use that to kickoff the widget lifecycle by gathering the template and data needed to instantiate the widget:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetinstall”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Installing &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;widget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">initializeWidget</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>The event argument comes in with details of the specific widget being instantiated (as <code>event.widget</code>). In the code above, you can see I’ve logged the widget’s <code>tag</code> value to the console. I pass the widget information over to my <code>initializeWidget()</code> function and it updates the widget with the latest data and, if necessary, sets up a <a href="https://developer.mozilla.org/docs/Web/API/Web_Periodic_Background_Synchronization_API">Periodic Background Sync</a>:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">initializeWidget</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">await</span><span class="token function">updateWidget</span><span class="token punctuation">(</span>widget<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">await</span><span class="token function">registerPeriodicSync</span><span class="token punctuation">(</span>widget<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>The code for my <code>updateWidget()</code> function is as follows:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">updateWidget</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">const</span> template <span class="token operator">=</span><span class="token keyword">await</span><span class="token punctuation">(</span><span class="token keyword">await</span><span class="token function">fetch</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>msAcTemplate<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> data <span class="token operator">=</span><span class="token keyword">await</span><span class="token punctuation">(</span><span class="token keyword">await</span><span class="token function">fetch</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>data<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">try</span><span class="token punctuation">{</span><span class="token keyword">await</span> self<span class="token punctuation">.</span>widgets<span class="token punctuation">.</span><span class="token function">updateByTag</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>tag<span class="token punctuation">,</span><span class="token punctuation">{</span> template<span class="token punctuation">,</span> data <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">catch</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Couldn’t update the widget &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>This function does the following:</p><ol><li>Get the template for this widget</li><li>Get the data to flow into the template</li><li>Use the <code>self.widgets.updateByTag()</code> method to push the <var>template</var> and <var>data</var> to the widget service to update any widget instances connected to the widget’s <code>tag</code>.</li></ol><p>As I mentioned, I also have code in place to take advantage of Periodic Background Sync if/when it’s available and the browser allows my site to do it:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">registerPeriodicSync</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">let</span> tag <span class="token operator">=</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>tag<span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token string">“update”</span><span class="token keyword">in</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">)</span><span class="token punctuation">{</span>registration<span class="token punctuation">.</span>periodicSync<span class="token punctuation">.</span><span class="token function">getTags</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">tags</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token comment">// only one registration per tag</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>tags<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>tag<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>periodicSync<span class="token punctuation">.</span><span class="token function">register</span><span class="token punctuation">(</span>tag<span class="token punctuation">,</span><span class="token punctuation">{</span><span class="token literal-property property">minInterval</span><span class="token operator">:</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>update<span class="token punctuation">,</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>This function also receives the widget details and:</p><ol><li>Looks to see if the widget <code>definition</code> (from the Manifest) includes an <code>update</code> member. If it has one, it…</li><li>Checks to see if there’s already a Periodic Background Sync that is registered for this tag. If none exists, it…</li><li>Registers a new Periodic Background Sync using the <code>tag</code> value and a minimum interval equal to the <code>update</code> requested.</li></ol><p>The <code>update</code> member, as you may recall, is the frequency (in seconds) you’d ideally like the widget to be updated. In reality, you’re at the mercy of the browser as to when (or even if) your sync will run, but that’s totally cool as there are other ways to update widgets as well.<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup></p><h3 id="uninstalling-a-widget" tabindex="-1"><a class="header-anchor" href="#uninstalling-a-widget" aria-hidden="true">#</a> Uninstalling a widget</h3><p>When a user uninstalls a widget, your Service Worker will receive a “widgetuninstall” event. Much like the “widgetinstall” event, the argument contains details about that widget which you can use to clean up after yourself:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetuninstall”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Uninstalling &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;widget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">uninstallWidget</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Your application may have different cleanup needs, but this is a great time to clean up any unneeded Periodic Sync registrations. Just be sure to check the length of the widget’s <code>instances</code> array (<code>widget.instances</code>) to make sure you’re dealing with the last instance of a given widget <em>before</em> you unregister the sync:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">uninstallWidget</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">if</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>instances<span class="token punctuation">.</span>length <span class="token operator">===</span><span class="token number">1</span><span class="token operator">&amp;&amp;</span><span class="token string">“update”</span><span class="token keyword">in</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">await</span> self<span class="token punctuation">.</span>registration<span class="token punctuation">.</span>periodicSync<span class="token punctuation">.</span><span class="token function">unregister</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>tag<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><h3 id="refreshing-your-widgets" tabindex="-1"><a class="header-anchor" href="#refreshing-your-widgets" aria-hidden="true">#</a> Refreshing your widgets</h3><p>Widget platforms may periodically freeze your widget(s) to save resources. For example, they may do this when widgets are not visible. To keep your widgets up to date, they will periodically issue a “widgetresume” event. If you’ve modeled your approach on the one I’ve outlined above, you can route this event right through to your <code>updateWidget()</code> function:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetresume”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Resuming &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;widget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">updateWidget</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="actions" tabindex="-1"><a class="header-anchor" href="#actions" aria-hidden="true">#</a> Actions</h3><p>While I don’t want to get too into the weeds here, I do want to mention that widgets can have predefined user actions as well. These actions result in “widget click” events being sent back to the Service Worker so you can respond to them:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetclick”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">const</span> widget <span class="token operator">=</span> event<span class="token punctuation">.</span>widget<span class="token punctuation">;</span><span class="token keyword">const</span> action <span class="token operator">=</span> event<span class="token punctuation">.</span>action<span class="token punctuation">;</span><span class="token keyword">switch</span><span class="token punctuation">(</span>action<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token comment">// Custom Actions</span><span class="token keyword">case</span><span class="token string">“refresh”</span><span class="token operator">:</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">updateWidget</span><span class="token punctuation">(</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">break</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>For a great example of how a widget can integrate actions, you should check out <a href="https://microsoftedge.github.io/Demos/pwamp/">the demo PWAmp project</a>. <a href="https://github.com/MicrosoftEdge/Demos/blob/main/pwamp/sw-widgets.js">Their Service Worker widget code</a> is worth a read.</p><h2 id="result!" tabindex="-1"><a class="header-anchor" href="#result!" aria-hidden="true">#</a> Result!</h2><p>With all of these pieces in place, I was excited to see my site showing up in the Widget Dashboard in Windows 11.</p><figure id="2023-10-09-02"><p><img src="https://www.aaron-gustafson.com/i/posts/2023-10-09/widgets-in-windows.jpg" alt=""></p><figcaption>A screenshot of Windows 11 showing the Widget Dashboard overlaying the desktop with this site installed as a PWA to the right. The “latest posts” and “latest links” widgets are shown.</figcaption></figure><p>You can view the full source code on GitHub:</p><ul><li><a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/w/feed.ac.json">“Feed” Adaptive Card Template</a></li><li><a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/manifest.json#L158-L237">Widget definitions in the Manifest</a></li><li><a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/_javascript/serviceworker/widgets.js">Widgets code in my Service Worker</a></li></ul><hr><p>I’m quite hopeful this will be the first of many places PWA-driven widgets will appear. If you’s like to see them supported elsewhere, be sure to tell your browser and OS vendor(s) of choice. The more they hear from their user base that this feature is needed, the more likely we are to see it get implemented in more places.</p><h2 id="addendum%3A-gotchas" tabindex="-1"><a class="header-anchor" href="#addendum%3A-gotchas" aria-hidden="true">#</a> Addendum: Gotchas</h2><p>In wiring this all up, I ran into a few current bugs I wanted to flag so you can avoid them:</p><ul><li>The <code>icons</code> member won’t accept SVG images. This should eventually be fixed, but it was keeping my widgets from appearing as installable.</li><li>The <code>screenshots</code> members can’t be incredibly large. I’m told you should provide square screenshots no larger than 500px ×500px.</li></ul><hr class="footnotes-sep"><section class="footnotes"><h4 class="hidden">Footnotes</h4><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>Have you checked out <a href="https://developer.mozilla.org/docs/Web/API/Server-sent_events/Using_server-sent_events">Server Events</a>? <a href="#fnref1" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content><amg:twitter><![CDATA[I finally had a chance to put the work I did on a widgets proposal for PWAs into practice on my own site. It’s pretty exciting!]]></amg:twitter><amg:summary><![CDATA[I finally had a chance to put the work I did on a widgets proposal for PWAs into practice on my own site. It’s pretty exciting!]]></amg:summary><summary type="html"><![CDATA[<p>I finally had a chance to put the work I did on a widgets proposal for PWAs into practice on my own site. It’s pretty exciting!</p>]]></summary><category term="progressive web apps" /><category term="experiments" /><category term="JavaScript" /><category term="Microsoft" /><category term="this site" /><category term="user experience" /><category term="web development" /><category term="web standards" /><category term="Windows" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/posts/2023-10-09/hero1.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/appearances/podcasts/2023-08-28-building-resilient-web-apps-progressive-enhancement-unveiled/</id><title type="html"><![CDATA[🎧 Building Resilient Web Apps: Progressive Enhancement Unveiled]]></title><link href="https://www.youtube.com/watch?v=-ZqsLwWM7YE" rel="alternate" type="text/html" /><published>2023-08-28T00:00:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Let’s delve into the world of progressive enhancement for web applications. From dissecting common pitfalls to offering invaluable tips, we unravel the art of building robust and inclusive web experiences.</p>]]></content><category term="progressive enhancement" /><category term="accessibility" /><category term="JavaScript" /><category term="progressive web apps" /><category term="web design" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/read-only-web-apps/</id><title type="html"><![CDATA[🔗 Read-only web apps]]></title><link href="https://www.aaron-gustafson.com/notebook/links/read-only-web-apps/" rel="alternate" type="text/html" /><link href="https://adactio.com/journal/20113" rel="related" type="text/html" /><published>2023-06-07T20:42:50Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I love Jeremy’s proposed compromise on JavaScript in web apps:</p><blockquote><p>Your app should work in a read-only mode without JavaScript.</p></blockquote>]]></content><amg:twitter><![CDATA[I love @adactio’s proposed compromise on web apps and JavaScript: “Your app should work in a read-only mode without JavaScript.”]]></amg:twitter><category term="progressive enhancement" /><category term="JavaScript" /><category term="web development" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://adactio.com/images/photo-300.jpg" /></entry></feed>