<?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: gioelevalori</title>
    <description>The latest articles on DEV Community by gioelevalori (@gioelevalori).</description>
    <link>https://dev.to/gioelevalori</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%2F757227%2F5d2915e1-6016-43b1-aad4-41f23eacf3b8.png</url>
      <title>DEV Community: gioelevalori</title>
      <link>https://dev.to/gioelevalori</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gioelevalori"/>
    <language>en</language>
    <item>
      <title>What I learned making an Angular component library work without zone.js</title>
      <dc:creator>gioelevalori</dc:creator>
      <pubDate>Tue, 03 Nov 2026 20:04:10 +0000</pubDate>
      <link>https://dev.to/gioelevalori/what-i-learned-making-an-angular-component-library-work-without-zonejs-56fl</link>
      <guid>https://dev.to/gioelevalori/what-i-learned-making-an-angular-component-library-work-without-zonejs-56fl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feq8p6hsinn33ky4defi8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feq8p6hsinn33ky4defi8.gif" alt="LiberaUI demo: accent colors, dark theme, data table and multi-select" width="760" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://liberaui.js.org" rel="noopener noreferrer"&gt;LiberaUI&lt;/a&gt;, a small MIT-licensed Angular component library. When Angular made zoneless change detection a first-class option, I assumed my components would just work. They didn't, and the bugs were quiet ones: no errors, just a screen that stopped updating.&lt;/p&gt;

&lt;p&gt;Here are the three patterns that broke, how I found them, and the tests that now keep them fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Values set from code never appeared
&lt;/h2&gt;

&lt;p&gt;Every form control in the library implements &lt;code&gt;ControlValueAccessor&lt;/code&gt;. With zone.js this was enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;writeValue&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="kr"&gt;string&lt;/span&gt; &lt;span class="o"&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&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;Without zone.js, &lt;code&gt;control.setValue('x')&lt;/code&gt; called &lt;code&gt;writeValue&lt;/code&gt;, the field changed, and the input on screen stayed empty. Nothing told Angular the view was dirty, because nothing in a template had triggered the change.&lt;/p&gt;

&lt;p&gt;The fix is one line, and it works on every Angular version the library supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;changeDetector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ChangeDetectorRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;writeValue&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="kr"&gt;string&lt;/span&gt; &lt;span class="o"&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&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;changeDetector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markForCheck&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 same applies to &lt;code&gt;setDisabledState&lt;/code&gt;. A form that disables itself after a request was silently ignoring it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Anything after a timer or a promise
&lt;/h2&gt;

&lt;p&gt;A copy button showed "Copied!" after &lt;code&gt;await navigator.clipboard.writeText(...)&lt;/code&gt;, and an entrance animation flipped its state inside &lt;code&gt;setTimeout&lt;/code&gt;. Both changed a plain field after the click handler had finished, so in a zoneless app the text never appeared and the animated content stayed at &lt;code&gt;opacity: 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For state that changes over time, a signal is the cleanest fix, because reading it in the template is enough for Angular to schedule an update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-in`&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;
  
  
  3. Tests that passed for the wrong reason
&lt;/h2&gt;

&lt;p&gt;The original tests called &lt;code&gt;fixture.detectChanges()&lt;/code&gt; after every action, which hides exactly this class of bug. The regression tests now run without zone.js and only wait for the app to settle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;TestBed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureTestingModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;HostComponent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;// provideExperimentalZonelessChangeDetection() before Angular 20&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;provideZonelessChangeDetection&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;componentInstance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenStable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I first ran them, 8 of 8 form controls failed. That number is the reason I'd recommend writing these tests before you trust a library, including mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on accessibility
&lt;/h2&gt;

&lt;p&gt;The same discipline paid off for keyboard and screen reader behavior. The menu follows the WAI-ARIA menu button pattern (arrow keys, Home/End, type-ahead, Escape returns focus), the data table supports multi-column sorting with Shift + Enter, and the breadcrumb marks the current page with &lt;code&gt;aria-current&lt;/code&gt; while drawing separators in CSS so they are not read aloud. Each of these has a regression test, and the keyboard ones dispatch real key events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docs and Theme Studio: &lt;a href="https://liberaui.js.org" rel="noopener noreferrer"&gt;https://liberaui.js.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zoneless starter in StackBlitz: &lt;a href="https://stackblitz.com/github/gioelevalori/liberaui/tree/master/examples/stackblitz" rel="noopener noreferrer"&gt;https://stackblitz.com/github/gioelevalori/liberaui/tree/master/examples/stackblitz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/gioelevalori/liberaui" rel="noopener noreferrer"&gt;https://github.com/gioelevalori/liberaui&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain components of your own, I'd be curious which zoneless bugs you ran into. And if you try LiberaUI, issues and feedback are very welcome.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>a11y</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
