<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jon Nyman</title>
    <description>The latest articles on DEV Community by Jon Nyman (@jon49).</description>
    <link>https://dev.to/jon49</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F145945%2F88338590-bbef-433a-b759-a4112f75ab9b.png</url>
      <title>DEV Community: Jon Nyman</title>
      <link>https://dev.to/jon49</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jon49"/>
    <language>en</language>
    <item>
      <title>_action: Declarative Event Handlers in an Attribute</title>
      <dc:creator>Jon Nyman</dc:creator>
      <pubDate>Thu, 25 Jun 2026 04:08:28 +0000</pubDate>
      <link>https://dev.to/jon49/action-declarative-event-handlers-in-an-attribute-3b35</link>
      <guid>https://dev.to/jon49/action-declarative-event-handlers-in-an-attribute-3b35</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/html-traits/"&gt;HTML Traits&lt;/a&gt; I went looking for a cleaner way to attach&lt;br&gt;
behavior to an element than wrapping it in a custom element. Traits got me most&lt;br&gt;
of the way there: composition over inheritance, multiple behaviors per element,&lt;br&gt;
no wrapper soup. But once I started building real pages I noticed that the&lt;br&gt;
&lt;em&gt;majority&lt;/em&gt; of the behavior I wanted wasn't really a "component" at all. It was&lt;br&gt;
just: &lt;em&gt;when this changes, submit the form&lt;/em&gt;. Or: &lt;em&gt;when this form submits, reset&lt;br&gt;
it&lt;/em&gt;. Reaching for a class with a constructor and a lifecycle to express "call&lt;br&gt;
this function on change" felt like too much ceremony.&lt;/p&gt;

&lt;p&gt;So I wrote something even smaller. It lives in&lt;br&gt;
&lt;a href="https://github.com/jon49/jon49-web/blob/master/lib/_action.ts" rel="noopener noreferrer"&gt;&lt;code&gt;_action.ts&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
and the whole thing is about 60 lines. Instead of defining a behavior and&lt;br&gt;
registering it, you name a function and bind it to an event with an attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;_change=&lt;/span&gt;&lt;span class="s"&gt;submit&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;post&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"?handler=editTeam"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When anything inside that form fires a &lt;code&gt;change&lt;/code&gt; event, the function named&lt;br&gt;
&lt;code&gt;submit&lt;/code&gt; runs. That's it. No custom element, no trait definition, no wrapper.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;There is a single set of delegated listeners on the document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findAttr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nf"&gt;handleCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;["click", "change", "submit"]&lt;/code&gt; list isn't special — it's just the set&lt;br&gt;
this app happens to use. The mechanism works for &lt;strong&gt;any bubbling event&lt;/strong&gt;, so if&lt;br&gt;
you want &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;keydown&lt;/code&gt;, &lt;code&gt;focusout&lt;/code&gt;, or a custom event of your own, you add&lt;br&gt;
its name to the list and you immediately get a matching &lt;code&gt;_input&lt;/code&gt; / &lt;code&gt;_keydown&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;_focusout&lt;/code&gt; attribute to bind against. Nothing else changes.&lt;/p&gt;

&lt;p&gt;For each event type it walks up from the target with &lt;code&gt;closest(&lt;/code&gt;[_${event}]&lt;code&gt;)&lt;/code&gt;&lt;br&gt;
to find the nearest &lt;code&gt;_click&lt;/code&gt; / &lt;code&gt;_change&lt;/code&gt; / &lt;code&gt;_submit&lt;/code&gt; attribute, with a fallback&lt;br&gt;
to the element's owning &lt;code&gt;form&lt;/code&gt;. The attribute's value is a space-separated list&lt;br&gt;
of function names that live on &lt;code&gt;window.app&lt;/code&gt;, and each one is called with a small&lt;br&gt;
context object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the listener is delegated at the document, it works for elements that&lt;br&gt;
were swapped in later (this app morphs HTML in from the server), and because the&lt;br&gt;
value can hold several names you get composition for free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;newPlayerName&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="na"&gt;_submit=&lt;/span&gt;&lt;span class="s"&gt;"reset clearAutoFocus"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registering a handler is just hanging a function off &lt;code&gt;window.app&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;clearAutoFocus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autofocus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a &lt;code&gt;_load&lt;/code&gt; attribute, handled separately, for the "run this once&lt;br&gt;
when the element appears" case. It fires on page load and again on &lt;code&gt;hz:completed&lt;/code&gt;&lt;br&gt;
(after server HTML is morphed in), then removes itself so it only runs once per&lt;br&gt;
element:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;hz:completed&lt;/code&gt; event comes from&lt;br&gt;
&lt;a href="https://github.com/jon49/htmz-be" rel="noopener noreferrer"&gt;htmz-be&lt;/a&gt;, a small library I wrote for&lt;br&gt;
morphing server HTML into the page. It was inspired by&lt;br&gt;
&lt;a href="https://github.com/Kalabasa/htmz" rel="noopener noreferrer"&gt;htmz&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/starfederation/datastar" rel="noopener noreferrer"&gt;Datastar&lt;/a&gt;. It dispatches&lt;br&gt;
&lt;code&gt;hz:completed&lt;/code&gt; after a swap, which is the hook &lt;code&gt;_action&lt;/code&gt; uses to re-run&lt;br&gt;
&lt;code&gt;_load&lt;/code&gt; handlers on freshly inserted elements.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[_load]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;handleCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Most of the time it's this simple
&lt;/h2&gt;

&lt;p&gt;In the soccer app the overwhelming majority of interactivity is a one-liner&lt;br&gt;
attribute. Auto-submit a settings form as you edit it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;_change=&lt;/span&gt;&lt;span class="s"&gt;submit&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;post&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"?handler=editTeam&amp;amp;teamId=${team.id}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reset the "add player" form after it submits, and drop the autofocus once&lt;br&gt;
you've used it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;_submit=&lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;post&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"?handler=addPlayer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;newPlayerName&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="na"&gt;autofocus&lt;/span&gt; &lt;span class="na"&gt;_submit=&lt;/span&gt;&lt;span class="s"&gt;clearAutoFocus&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these needed a component, a lifecycle, or a definition. The function is&lt;br&gt;
named, the attribute points at it, done.&lt;/p&gt;
&lt;h2&gt;
  
  
  When it gets more involved: the game timer
&lt;/h2&gt;

&lt;p&gt;The interesting case is the live game timer. A &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; that ticks up every&lt;br&gt;
second, can flash, can be paused (static), and ticks for several elements at&lt;br&gt;
once on the page. Here &lt;code&gt;_action&lt;/code&gt; is just the &lt;em&gt;entry point&lt;/em&gt; — the &lt;code&gt;_load&lt;/code&gt;&lt;br&gt;
attribute instantiates a real class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;_load=&lt;/span&gt;&lt;span class="s"&gt;"gameTimer"&lt;/span&gt; &lt;span class="na"&gt;data-total=&lt;/span&gt;&lt;span class="s"&gt;"${total}"&lt;/span&gt; &lt;span class="na"&gt;data-static&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;00:00&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gameTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;   &lt;span class="c1"&gt;// guard against double-init&lt;/span&gt;
  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GameTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;GameTimer&lt;/code&gt; class then does everything a stateful component does — but&lt;br&gt;
notice it has to &lt;em&gt;assemble its own lifecycle&lt;/em&gt; out of parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameTimer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hz:completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disconnectWatcher&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- borrowed teardown&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;disconnectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hz:completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things stand out compared to the simple case:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Connected" is manual.&lt;/strong&gt; &lt;code&gt;_load&lt;/code&gt; is the moment of construction, and the
&lt;code&gt;el._&lt;/code&gt; guard exists because, unlike a web component, nothing stops the same
element from being initialized twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Disconnected" is borrowed.&lt;/strong&gt; There's no native callback when the element
leaves the DOM, so the app provides a &lt;code&gt;disconnectWatcher&lt;/code&gt; — a single
&lt;code&gt;MutationObserver&lt;/code&gt; on &lt;code&gt;document.body&lt;/code&gt; that calls &lt;code&gt;disconnectedCallback()&lt;/code&gt; on
registered instances when their node is removed. The timer leans on that to
clean up its &lt;code&gt;setInterval&lt;/code&gt; and its event listener.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the honest tradeoff: &lt;code&gt;_action&lt;/code&gt; gives you next to nothing for free, so&lt;br&gt;
when you &lt;em&gt;do&lt;/em&gt; need real component semantics you reassemble them yourself from a&lt;br&gt;
load hook, a guard flag, and a shared disconnect watcher. For one timer that's&lt;br&gt;
fine. If half your page were stateful widgets, you'd feel the missing&lt;br&gt;
lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the three
&lt;/h2&gt;

&lt;p&gt;All three solve the same underlying problem — &lt;em&gt;attach behavior to HTML without&lt;br&gt;
an SPA framework&lt;/em&gt; — but they sit at different points on the&lt;br&gt;
simplicity-vs-encapsulation curve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web components
&lt;/h3&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real, browser-native lifecycle: &lt;code&gt;connectedCallback&lt;/code&gt;,
&lt;code&gt;disconnectedCallback&lt;/code&gt;, &lt;code&gt;attributeChangedCallback&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Encapsulation, optional Shadow DOM, and a proper custom element registry.&lt;/li&gt;
&lt;li&gt;Standard — no library to ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom element names must contain a dash, and you generally wrap your
semantic HTML in a non-semantic element.&lt;/li&gt;
&lt;li&gt;Built-in/customized elements (the &lt;code&gt;is=&lt;/code&gt; form) aren't supported in Safari, and
even there you get only one behavior per element.&lt;/li&gt;
&lt;li&gt;You re-learn and re-implement APIs that native elements already give you, and
composing several behaviors means nesting several elements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  HTML Traits
&lt;/h3&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composition: &lt;code&gt;traits="elastic-textarea character-limit"&lt;/code&gt;, multiple,
ordered behaviors on &lt;strong&gt;one&lt;/strong&gt; element, no wrappers.&lt;/li&gt;
&lt;li&gt;Keeps the native element (and its native attributes like &lt;code&gt;maxlength&lt;/code&gt;), so you
learn less and reimplement nothing.&lt;/li&gt;
&lt;li&gt;Built for progressive enhancement; mirrors the web-component class API
(&lt;code&gt;constructor(el)&lt;/code&gt;, &lt;code&gt;disconnectedCallback&lt;/code&gt;) so it's familiar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's still "components": you define a class and register it for every
behavior, which is more than some interactions deserve.&lt;/li&gt;
&lt;li&gt;A library you ship, and a partial reimplementation of web components — no
Shadow DOM, never a true custom element.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  _action
&lt;/h3&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The smallest of the three (~60 lines, one set of delegated listeners). No
per-element registration, no definitions — just name a function on
&lt;code&gt;window.app&lt;/code&gt; and point an attribute at it.&lt;/li&gt;
&lt;li&gt;Naturally composable (&lt;code&gt;_submit="reset clearAutoFocus"&lt;/code&gt;) and form-aware (it
falls back to the owning form), which is exactly what MPA-style pages need.&lt;/li&gt;
&lt;li&gt;Works with server-morphed HTML out of the box because it's delegated at the
document.&lt;/li&gt;
&lt;li&gt;Right-sized for the common case, which is "run this function on this event,"
not "instantiate a stateful component."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No lifecycle by default. Initialization is a manual &lt;code&gt;_load&lt;/code&gt; hook (plus a guard
flag to avoid double-init), and teardown only exists if you opt into the
shared &lt;code&gt;disconnectWatcher&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No encapsulation — every handler is a global on &lt;code&gt;window.app&lt;/code&gt;, so naming is
on you.&lt;/li&gt;
&lt;li&gt;It's event handlers, not components: for genuinely stateful widgets (the game
timer) you end up rebuilding the very lifecycle that traits and web components
hand you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which one when?
&lt;/h2&gt;

&lt;p&gt;I think of it as a ladder, not a competition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_action&lt;/code&gt;&lt;/strong&gt; for the 90% that is "on this event, call this function" —
auto-submit, reset, confirm, toggle a class. It's the default reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML Traits&lt;/strong&gt; when a behavior is genuinely a reusable, stateful enhancement
of a native element and you want clean composition without wrappers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web components&lt;/strong&gt; when you need true encapsulation, Shadow DOM, or you're
shipping a widget for other people's pages to consume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern I keep coming back to is that most "interactivity" on a server-driven&lt;br&gt;
page isn't a component at all. &lt;code&gt;_action&lt;/code&gt; lets me stop pretending it is.&lt;/p&gt;




&lt;p&gt;This was originally posted on &lt;a href="https://jnyman.com" rel="noopener noreferrer"&gt;jnyman.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>progressiveenhancement</category>
      <category>webcomponents</category>
      <category>html</category>
      <category>javascript</category>
    </item>
    <item>
      <title>HTML Traits</title>
      <dc:creator>Jon Nyman</dc:creator>
      <pubDate>Wed, 06 Aug 2025 05:18:36 +0000</pubDate>
      <link>https://dev.to/jon49/html-traits-ea2</link>
      <guid>https://dev.to/jon49/html-traits-ea2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Imagine you want to add multiple interactive behaviors to a single HTML element—maybe an auto-expanding textarea that also validates character limits. With traditional web components, you'd end up with nested wrapper elements cluttering your markup, or you'd be limited to Safari's refusal to support built-in web components.&lt;/p&gt;

&lt;p&gt;What if you could simply write &lt;code&gt;&amp;lt;textarea traits="elastic character-limit"&amp;gt;&lt;/code&gt; and be done?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jon49/html-traits" rel="noopener noreferrer"&gt;HTML Traits&lt;/a&gt; is a lightweight JavaScript library that makes this possible. It adds multiple behaviors to existing HTML elements through a simple &lt;code&gt;traits&lt;/code&gt; attribute, giving you clean, composable functionality without the wrapper element overhead.&lt;/p&gt;

&lt;p&gt;While built-in web components offer progressive enhancement through the &lt;code&gt;is&lt;/code&gt; attribute, they come with significant limitations: Safari doesn't support them, you can only add one behavior per element, and behaviors are tied to specific element types. Regular web components solve some of these issues but force you to wrap your semantic HTML in unnecessary container elements.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML Traits
&lt;/h3&gt;

&lt;p&gt;Enter &lt;a href="https://github.com/jon49/html-traits" rel="noopener noreferrer"&gt;&lt;code&gt;html-traits&lt;/code&gt;&lt;/a&gt;. This little library&lt;br&gt;
makes it so you can add multiple behaviors to a single element. It makes it so&lt;br&gt;
you don't need nesting of elements. You just add an attribute. No need for&lt;br&gt;
definitions to be defined with a dash. Some advantages are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Composition over inheritance.&lt;/li&gt;
&lt;li&gt;Multiple traits to one element.&lt;/li&gt;
&lt;li&gt;Ordered traits.&lt;/li&gt;
&lt;li&gt;Built for progressive enhancement.&lt;/li&gt;
&lt;li&gt;No need for extra elements on your page just to get new runtime behavior.&lt;/li&gt;
&lt;li&gt;Cleaner HTML.&lt;/li&gt;
&lt;li&gt;No need to reimplement native element behavior as you just keep the original
element.&lt;/li&gt;
&lt;li&gt;Use native HTML for your elements rather than making everything custom and
needing to learn custom APIs.&lt;/li&gt;
&lt;li&gt;If you would like to use inheritance for helper functions that you use across
all your traits it is easy to do so. Although composition over inheritance is
desirable sometimes inheritance is the best solution.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A use case of this pattern could be a &lt;code&gt;textarea&lt;/code&gt; element. We would like it to&lt;br&gt;
automatically expand as you type. The old way, using web components, would be to&lt;br&gt;
do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;elastic-textarea&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;textarea&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/elastic-textarea&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But with HTML Traits:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;But now we would also like to make the &lt;code&gt;textarea&lt;/code&gt; turn red, if there are more&lt;br&gt;
than 250 characters. The old way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;elastic-textarea&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;character-limit&lt;/span&gt; &lt;span class="na"&gt;data-character-limit=&lt;/span&gt;&lt;span class="s"&gt;"250"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;textarea&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/character-limit&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/elastic-textarea&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;traits=&lt;/span&gt;&lt;span class="s"&gt;"elastic-textarea character-limit"&lt;/span&gt; &lt;span class="na"&gt;maxlength=&lt;/span&gt;&lt;span class="s"&gt;"250"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes making MPA-first applications much easier and cleaner with reusable&lt;br&gt;
functionality written in a declarative way.&lt;/p&gt;
&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;HTML Traits is built specifically for progressive enhancement. It probably isn't&lt;br&gt;
the right library for you if you want to go all into JS. It also doesn't work&lt;br&gt;
with the Shadow DOM. Neither is it a full implementation of web components.&lt;br&gt;
Although I'm trying to follow the same API as web components, it will never be a&lt;br&gt;
true web component.&lt;/p&gt;
&lt;h2&gt;
  
  
  A working example
&lt;/h2&gt;

&lt;p&gt;Here's a full implementation of the component &lt;code&gt;character-limit&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CharacterLimit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Expected an HTMLTextAreaElement, but received: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for 'character-limit'.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;maxlength&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid 'maxlength' attribute on element: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Expected a number.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxLength&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outlineColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;disconnectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Here we can clean up any resources or event listeners.&lt;/span&gt;
        &lt;span class="c1"&gt;// In this case we do not need to remove the event listener since it&lt;/span&gt;
        &lt;span class="c1"&gt;// will be automatically garbage collected.&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`CharacterLimit trait disconnected from element: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]?.(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outlineColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outlineColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxLength&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outlineColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Initialize the trait&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineTrait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;character-limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CharacterLimit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And below is the HTML. Notice how the native attribute &lt;code&gt;maxlength&lt;/code&gt; is used? It&lt;br&gt;
is nice to only need to learn as little APIs as possible for a code base. Just&lt;br&gt;
learn HTML and use what is already native! Also, notice in the code above, I&lt;br&gt;
didn't need to implement cutting the text down, it was automatically implemented&lt;br&gt;
by the browser with the attribute &lt;code&gt;maxlength&lt;/code&gt;!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./html-traits.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./character-limit.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;traits=&lt;/span&gt;&lt;span class="s"&gt;"character-limit"&lt;/span&gt; &lt;span class="na"&gt;maxlength=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was originally published on &lt;a href="https://jnyman.com/html-traits/" rel="noopener noreferrer"&gt;jnyman.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>progressiveenhancement</category>
      <category>webcomponents</category>
      <category>html</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
