{"id":4241,"date":"2026-02-26T09:56:00","date_gmt":"2026-02-26T08:56:00","guid":{"rendered":"https:\/\/nguenkam.com\/blog\/?p=4241"},"modified":"2026-02-26T09:56:00","modified_gmt":"2026-02-26T08:56:00","slug":"angular-signal-forms-config","status":"publish","type":"post","link":"https:\/\/nguenkam.com\/blog\/index.php\/2026\/02\/26\/angular-signal-forms-config\/","title":{"rendered":"Angular Signal Forms Config"},"content":{"rendered":"\n<p>Angular 21 introduced <strong>Signal Forms<\/strong> (experimental). They align forms with Angular\u2019s signal-based reactivity model and generally aim for a <strong>cleaner, more explicit DOM<\/strong>.<\/p>\n\n\n\n<p>One practical consequence: <strong>Signal Forms do <em>not<\/em> add the classic Angular CSS state classes by default<\/strong>.<\/p>\n\n\n\n<p>That means you won\u2019t automatically see classes like:<\/p>\n\n\n\n<ul><li><code>ng-valid<\/code>&nbsp;\/&nbsp;<code>ng-invalid<\/code><\/li><li><code>ng-pristine<\/code>&nbsp;\/&nbsp;<code>ng-dirty<\/code><\/li><li><code>ng-untouched<\/code>&nbsp;\/&nbsp;<code>ng-touched<\/code><\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>Why the config is needed<\/h4>\n\n\n\n<h5>1) Backward compatibility (the big one)<\/h5>\n\n\n\n<p>Many applications\u2014especially larger or older ones\u2014have global or shared styles such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>input.ng-invalid.ng-touched {\r\n  border: 1px solid #e00;\r\n}\r<\/code><\/pre>\n\n\n\n<p>When you migrate a form to Signal Forms and the classes are missing, your UI can \u201cbreak\u201d silently:<\/p>\n\n\n\n<ul><li>invalid inputs might no longer show red borders<\/li><li>touched\/dirty styling disappears<\/li><li>error messages driven by CSS selectors stop working<\/li><\/ul>\n\n\n\n<p><strong>Signal Forms Config lets you restore that behavior globally<\/strong> without rewriting templates everywhere.<\/p>\n\n\n\n<h5>2) Custom design systems (a modern use case)<\/h5>\n\n\n\n<p>Even if you don\u2019t care about <code>ng-*<\/code> classes, you might want <strong>your own semantic classes<\/strong> that match your design system:<\/p>\n\n\n\n<ul><li><code>ds-input--error<\/code><\/li><li><code>ds-input--touched<\/code><\/li><li><code>field--dirty<\/code><\/li><\/ul>\n\n\n\n<p>Signal Forms Config lets you generate these classes reactively based on signal form state\u2014centrally, consistently, and without template noise.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>When to use it<\/h4>\n\n\n\n<p>Use <strong><code>provideSignalFormsConfig(...)<\/code><\/strong> when:<\/p>\n\n\n\n<ul><li>You are migrating from Reactive Forms and rely on&nbsp;<code>.ng-invalid<\/code>,&nbsp;<code>.ng-touched<\/code>, etc.<\/li><li>You have shared\/global CSS expecting Angular\u2019s legacy state classes<\/li><li>You want to standardize styling across the app using your own class names<\/li><li>You want a \u201cset it once\u201d solution rather than repeating&nbsp;<code>[class...]<\/code>&nbsp;bindings on every input<\/li><\/ul>\n\n\n\n<p>You might skip it when:<\/p>\n\n\n\n<ul><li>your styling is fully component-driven (e.g., you only use explicit template bindings or wrapper components)<\/li><li>you prefer a minimal DOM and don\u2019t need state classes at all<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>How it works (conceptually)<\/h4>\n\n\n\n<p>Signal Forms maintain state (valid\/invalid\/touched\/dirty, etc.) as <strong>signals<\/strong>.<br>The config lets Angular <strong>map state ? CSS classes<\/strong> and apply them to form controls automatically.<\/p>\n\n\n\n<h5>Option A: Bring back the classic&nbsp;<code>ng-*<\/code>&nbsp;classes (migration-friendly)<\/h5>\n\n\n\n<p>Add a global provider (typically in <code>app.config.ts<\/code> for standalone apps):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { ApplicationConfig } from '@angular\/core';\r\nimport { provideSignalFormsConfig, NG_STATUS_CLASSES } from '@angular\/forms';\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: &#91;\r\n    provideSignalFormsConfig({\r\n      classes: NG_STATUS_CLASSES,\r\n    }),\r\n  ],\r\n};\r<\/code><\/pre>\n\n\n\n<h5>Result<\/h5>\n\n\n\n<p>Your Signal Form controls will again receive the familiar classes like:<\/p>\n\n\n\n<ul><li><code>ng-invalid<\/code>&nbsp;when invalid<\/li><li><code>ng-touched<\/code>&nbsp;after blur<\/li><li><code>ng-dirty<\/code>&nbsp;after value changes<\/li><\/ul>\n\n\n\n<p>This is the fastest way to migrate without touching CSS.<\/p>\n\n\n\n<h5>Option B: Emit your own custom classes (design-system-friendly)<\/h5>\n\n\n\n<p>Instead of <code>NG_STATUS_CLASSES<\/code>, provide a class mapping. Each entry decides whether a class should be present based on the current state signal.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { provideSignalFormsConfig } from '@angular\/forms';\r\n\r\nexport const appConfig = {\r\n  providers: &#91;\r\n    provideSignalFormsConfig({\r\n      classes: {\r\n        'ds-input--error': ({ state }) => state().invalid(),\r\n        'ds-input--touched': ({ state }) => state().touched(),\r\n        'ds-input--dirty': ({ state }) => state().dirty(),\r\n        'ds-input--valid': ({ state }) => state().valid(),\r\n      },\r\n    }),\r\n  ],\r\n};\r<\/code><\/pre>\n\n\n\n<h5>What you gain<\/h5>\n\n\n\n<ul><li>Your HTML stays clean (no repeated&nbsp;<code>[class...]<\/code>&nbsp;bindings)<\/li><li>Styling rules become consistent across the entire app<\/li><li>You can align form states with your naming conventions<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>A detailed example (end-to-end)<\/h4>\n\n\n\n<h5>1) Global config (custom classes)<\/h5>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.config.ts\r\nimport { ApplicationConfig } from '@angular\/core';\r\nimport { provideSignalFormsConfig } from '@angular\/forms';\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: &#91;\r\n    provideSignalFormsConfig({\r\n      classes: {\r\n        'field--invalid': ({ state }) => state().invalid(),\r\n        'field--touched': ({ state }) => state().touched(),\r\n      },\r\n    }),\r\n  ],\r\n};\r<\/code><\/pre>\n\n\n\n<h5>2) Component: create a Signal Forms control + bind it in the template (no manual class bindings)<\/h5>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ email.component.ts (conceptual)\r\nimport { Component } from '@angular\/core';\r\n\/\/ Signal Forms APIs are experimental; names can differ depending on Angular version\/build.\r\nimport { signalControl, Validators } from '@angular\/forms';\r\n\r\n@Component({\r\n  selector: 'app-email',\r\n  templateUrl: '.\/email.component.html',\r\n})\r\nexport class EmailComponent {\r\n  email = signalControl('', {\r\n    validators: &#91;Validators.required, Validators.email],\r\n  });\r\n}\r<\/code><\/pre>\n\n\n\n<p>You write your inputs normally. Angular applies classes automatically based on signal form state.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- email.component.html (conceptual) -->\r\n&lt;label>\r\n  Email\r\n  &lt;input type=\"email\" &#91;control]=\"email\" \/>\r\n&lt;\/label>\r\n\r<\/code><\/pre>\n\n\n\n<h5>3) CSS reacts to state<\/h5>\n\n\n\n<pre class=\"wp-block-code\"><code>input.field--invalid.field--touched {\r\n  border: 1px solid #d00;\r\n  outline: none;\r\n}\r<\/code><\/pre>\n\n\n\n<p>Now:<\/p>\n\n\n\n<ul><li>before the user interacts: no \u201ctouched\u201d class ? no red border<\/li><li>after blur:&nbsp;<code>field--touched<\/code>&nbsp;appears<\/li><li>if invalid at that time:&nbsp;<code>field--invalid<\/code>&nbsp;also appears ? red border shows<\/li><\/ul>\n\n\n\n<p>All without adding template logic.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>Why the API uses a config object (not many parameters)<\/h4>\n\n\n\n<p><code>provideSignalFormsConfig({ ... })<\/code> takes an object so Angular can add more options later (beyond CSS classes) <strong>without breaking the API<\/strong>. This matters because Signal Forms are experimental and likely to evolve.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Angular 21 introduced Signal Forms (experimental). They align forms with Angular\u2019s signal-based reactivity model and generally aim for a cleaner, more explicit DOM. One practical consequence: Signal Forms do not add the classic Angular CSS state classes by default. That means you won\u2019t automatically see classes like: ng-valid&nbsp;\/&nbsp;ng-invalid ng-pristine&nbsp;\/&nbsp;ng-dirty ng-untouched&nbsp;\/&nbsp;ng-touched Why the config is needed [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4242,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[37,1038],"tags":[1102,1118,1117,1120,1122,1119,1104,1121],"_links":{"self":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4241"}],"collection":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=4241"}],"version-history":[{"count":1,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4241\/revisions"}],"predecessor-version":[{"id":4243,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4241\/revisions\/4243"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media\/4242"}],"wp:attachment":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=4241"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=4241"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=4241"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}