<?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: Zonaib Bokhari</title>
    <description>The latest articles on DEV Community by Zonaib Bokhari (@zonaibbokhari).</description>
    <link>https://dev.to/zonaibbokhari</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%2F294281%2F1419f07a-19f3-49bb-8a00-5915359d5e3b.jpg</url>
      <title>DEV Community: Zonaib Bokhari</title>
      <link>https://dev.to/zonaibbokhari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zonaibbokhari"/>
    <language>en</language>
    <item>
      <title>Ef-timelapse: a time-travel viewer for your EF Core schema published</title>
      <dc:creator>Zonaib Bokhari</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:29:14 +0000</pubDate>
      <link>https://dev.to/zonaibbokhari/ef-timelapse-a-time-travel-viewer-for-your-ef-core-schema-published-49m7</link>
      <guid>https://dev.to/zonaibbokhari/ef-timelapse-a-time-travel-viewer-for-your-ef-core-schema-published-49m7</guid>
      <description>&lt;p&gt;A few weeks ago I was staring at a table in a database, trying to figure out when a column showed up and why a particular foreign key existed. The answer was in there somewhere, spread across thirty-odd migration files, and finding it meant opening them one by one. EF Core migrations are great at telling you &lt;em&gt;what&lt;/em&gt; changed — one file, one point in time. They're not great at telling you &lt;em&gt;how&lt;/em&gt; something evolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update: v0.2.0 adds an entity relationship visualizer
&lt;/h2&gt;

&lt;p&gt;Since publishing the first version, I added a Git-mode entity relationship graph for scaffolded EF/database-first projects. The tool no longer only answers “what changed in this entity over time?” It can now also answer “how is this entity connected to the rest of the model?”&lt;/p&gt;

&lt;p&gt;In Git scaffold mode, &lt;code&gt;ef-timelapse&lt;/code&gt; now infers relationships from navigation properties and shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a relationship graph overview of the model&lt;/li&gt;
&lt;li&gt;a focused entity view with the selected entity’s properties and direct relationships&lt;/li&gt;
&lt;li&gt;a timeline filtered to only the checkpoints where that focused entity changed&lt;/li&gt;
&lt;li&gt;relationship tooltips in plain language, for example &lt;code&gt;Order.Items one-to-many OrderItem&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;zoom, pan, and resizable focused cards for larger models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially useful for database-first projects that do not have EF migrations. You can point the tool at the scaffolded project, scrub through Git history, and inspect both the property changes and relationship shape over time.&lt;/p&gt;

&lt;p&gt;Release: &lt;a href="https://github.com/xonaib/ef-timelapse/releases/tag/v0.2.0" rel="noopener noreferrer"&gt;https://github.com/xonaib/ef-timelapse/releases/tag/v0.2.0&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Repository: &lt;a href="https://github.com/xonaib/ef-timelapse" rel="noopener noreferrer"&gt;https://github.com/xonaib/ef-timelapse&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relationship graph overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fkcqn3ralpl7zpv25c7zy.png" class="article-body-image-wrapper"&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%2Farticles%2Fkcqn3ralpl7zpv25c7zy.png" alt="Relationship graph overview" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focused entity graph&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2F99drqyb3h62m2a6p23nz.png" class="article-body-image-wrapper"&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%2Farticles%2F99drqyb3h62m2a6p23nz.png" alt="Focused entity graph" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relationship tooltip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fbizh3njrxe10o9s25be6.png" class="article-body-image-wrapper"&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%2Farticles%2Fbizh3njrxe10o9s25be6.png" alt="Relationship tooltip" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I built a small tool to fix that for myself: &lt;code&gt;ef-timelapse&lt;/code&gt;. It replays your EF Core schema history — either from migration files, or from Git history if you're on a scaffolded/database-first setup with no migrations at all — into one browser-based timeline you can scrub through, search, and diff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; ef-timelapse
ef-timelapse serve C:&lt;span class="se"&gt;\P&lt;/span&gt;ath&lt;span class="se"&gt;\T&lt;/span&gt;o&lt;span class="se"&gt;\Y&lt;/span&gt;ourProject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's really the whole setup. It figures out on its own whether your project uses migrations or is a scaffolded project tracked in Git, and serves the same interactive viewer either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to look at schema history
&lt;/h2&gt;

&lt;p&gt;If you've got a &lt;code&gt;Migrations&lt;/code&gt; folder, &lt;strong&gt;migration mode&lt;/strong&gt; parses every file with Roslyn and replays each &lt;code&gt;migrationBuilder&lt;/code&gt; call in order. You get a slider that walks through every migration, showing exactly which tables and columns it touched at each step:&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fditnzaiihsluaba65smt.png" class="article-body-image-wrapper"&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%2Farticles%2Fditnzaiihsluaba65smt.png" alt="Migration checkpoint view" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you don't — a lot of database-first projects scaffold their model classes straight from an existing database and never touch migrations — &lt;strong&gt;Git scaffold history mode&lt;/strong&gt; walks your Git history instead and replays how the generated model files changed commit by commit. Same idea, different source of truth.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fnxrmv0kg167e5cpwb48t.png" class="article-body-image-wrapper"&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%2Farticles%2Fnxrmv0kg167e5cpwb48t.png" alt="Git mode: commit timeline and diff view" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both modes drop you into the same viewer. Same slider, same search box, same detail panels — just backed by different data underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding when something actually changed
&lt;/h2&gt;

&lt;p&gt;The feature I built for myself and now use the most is the per-entity (or per-table, in migration mode) history panel. Instead of clicking through commits one at a time hoping to spot when a property got added, you search for the class or table by name and get every point in history it changed, newest first:&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fuhnd8imhtr5i8xi2liys.png" class="article-body-image-wrapper"&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%2Farticles%2Fuhnd8imhtr5i8xi2liys.png" alt="Property timeline for a searched entity" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same thing works for tables and columns in migration mode — search "Orders" and see every migration that touched it, in one place, instead of opening files one by one:&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2F69mnmulfwwmn76tajctu.png" class="article-body-image-wrapper"&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%2Farticles%2F69mnmulfwwmn76tajctu.png" alt="Table column history panel" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It shows you the actual diff, not just a summary
&lt;/h2&gt;

&lt;p&gt;In Git mode, clicking a changed file at any commit shows the real inline diff — added and removed lines highlighted — not just "+12 -3" and nothing else:&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fj2tsqt463z8aboqfv4bt.png" class="article-body-image-wrapper"&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%2Farticles%2Fj2tsqt463z8aboqfv4bt.png" alt="Diff view with add/remove highlighting" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it hold up on a real codebase?
&lt;/h2&gt;

&lt;p&gt;I didn't want to just demo this against a toy project with five commits, so I pointed it at &lt;a href="https://github.com/dotnet-architecture/eShopOnWeb" rel="noopener noreferrer"&gt;eShopOnWeb&lt;/a&gt;, Microsoft's reference ASP.NET Core + EF Core sample app — 455 real commits, real entities, real migrations. Every screenshot in this post is from that run. The first time you point Git mode at a repository like that, it has to walk the full history and index it, so you'll see a progress modal while that happens:&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2Fkvrsi0n24xhasv9x3yvv.png" class="article-body-image-wrapper"&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%2Farticles%2Fkvrsi0n24xhasv9x3yvv.png" alt="Indexing progress modal" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that first pass it's cached on disk, so running it again against the same repo starts instantly. If you're actively working on the project, &lt;code&gt;--watch&lt;/code&gt; picks up file changes live too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately doesn't do
&lt;/h2&gt;

&lt;p&gt;I'd rather it tell me "I don't know" than guess wrong. If a migration operation depends on something the parser can't statically resolve — a local variable, a helper method call, raw SQL, a branch in the code — it gets reported as an unsupported step with the exact source line, instead of being silently skipped or guessed at. It also only replays the forward path (&lt;code&gt;Up()&lt;/code&gt;); rollback history isn't visualized. This is a read-only exploration tool, not a migration generator.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; ef-timelapse
ef-timelapse serve &amp;lt;path-to-your-project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's open source, MIT-licensed: &lt;a href="https://github.com/xonaib/ef-timelapse" rel="noopener noreferrer"&gt;github.com/xonaib/ef-timelapse&lt;/a&gt;. If you run it against your own project and it chokes on a migration pattern or gets a schema shape wrong, I'd genuinely like to hear about it — open an issue, or a PR if you've already got a fix in mind.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>entityframework</category>
      <category>opensource</category>
    </item>
    <item>
      <title>ng-loom: a dependency graph for your Angular app, as a single HTML file</title>
      <dc:creator>Zonaib Bokhari</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:12:57 +0000</pubDate>
      <link>https://dev.to/zonaibbokhari/ng-loom-a-dependency-graph-for-your-angular-app-as-a-single-html-file-4o2d</link>
      <guid>https://dev.to/zonaibbokhari/ng-loom-a-dependency-graph-for-your-angular-app-as-a-single-html-file-4o2d</guid>
      <description>&lt;p&gt;On a large Angular workspace at work, I needed to know what actually depended on a shared service before I touched it. &lt;code&gt;grep&lt;/code&gt; finds text, not relationships. The routing config tells you what loads, not what it pulls in. And most "architecture diagram" tools want your code in the cloud, or want you to hand-maintain a diagram that's stale within a week.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;ng-loom&lt;/strong&gt; — a CLI that statically analyzes an Angular workspace and spits out a single, self-contained HTML file with the whole dependency graph in it. No server, no account, no uploading your source anywhere. You run it, you get a report, you open it in a browser.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; &lt;code&gt;ng-loom&lt;/code&gt; &lt;code&gt;0.2.0&lt;/code&gt; is now published.&lt;/p&gt;

&lt;p&gt;The new release improves correctness and safer sharing. Reports can now be generated with &lt;code&gt;--portable&lt;/code&gt; to strip local absolute paths and source snippets, template parsing prefers the target project's installed &lt;code&gt;@angular/compiler&lt;/code&gt;, duplicate class/import/DI/route references resolve by declaration identity, and ambiguous template selectors are surfaced with heuristic confidence instead of silently picking one.&lt;/p&gt;

&lt;p&gt;Also corrected: current lazy route support covers &lt;code&gt;loadComponent&lt;/code&gt;; &lt;code&gt;loadChildren&lt;/code&gt; is not supported yet. Nx/full monorepo workspace support is still planned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;Point it at an Angular CLI project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ng-loom ./path/to/angular-project &lt;span class="nt"&gt;--out&lt;/span&gt; report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it walks the workspace with &lt;code&gt;ts-morph&lt;/code&gt;, finds every component, directive, pipe, service, and NgModule, and maps out how they connect: standalone &lt;code&gt;imports&lt;/code&gt;, NgModule &lt;code&gt;imports&lt;/code&gt;/&lt;code&gt;exports&lt;/code&gt;/&lt;code&gt;declarations&lt;/code&gt;, constructor DI and &lt;code&gt;inject()&lt;/code&gt; calls, &lt;code&gt;hostDirectives&lt;/code&gt;, lazy routes via &lt;code&gt;loadComponent&lt;/code&gt;. Template usage isn't regex-matched against your HTML — it runs your actual templates through &lt;code&gt;@angular/compiler&lt;/code&gt;'s real parser, so &lt;code&gt;@if&lt;/code&gt;/&lt;code&gt;@for&lt;/code&gt;/&lt;code&gt;@switch&lt;/code&gt;/&lt;code&gt;@defer&lt;/code&gt; blocks, structural directives, and attribute selectors all resolve correctly instead of getting silently missed.&lt;/p&gt;

&lt;p&gt;Every edge in the graph carries the file, line, and column it came from, plus a &lt;code&gt;vscode://&lt;/code&gt; link straight to the source. So when the report tells you &lt;code&gt;ArticleComponent&lt;/code&gt; injects &lt;code&gt;UserService&lt;/code&gt;, you're not taking its word for it — you can jump to the exact line.&lt;/p&gt;

&lt;p&gt;The report itself has a few views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;folder/package overview&lt;/strong&gt; that treemaps your codebase into clusters, so you can see at a glance where the size and complexity actually live before drilling into a single file.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;node focus view&lt;/strong&gt; with a Details panel — imports, injected services, host directives, template usages, all with source snippets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace relation&lt;/strong&gt;, which answers "how does A actually reach B?" — it finds every path between two artifacts, not just the first one it stumbles on, which matters more than you'd think once you're a few layers of indirection deep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what that looks like against a real app (&lt;a href="https://github.com/akveo/ngx-admin" rel="noopener noreferrer"&gt;ngx-admin&lt;/a&gt;, 294 nodes, 340 edges):&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2F5epszzspgm9yptpjzwsl.png" class="article-body-image-wrapper"&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%2Farticles%2F5epszzspgm9yptpjzwsl.png" alt="Overview clusters" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Farticles%2F116ab946wy9ihoya749p.png" class="article-body-image-wrapper"&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%2Farticles%2F116ab946wy9ihoya749p.png" alt="Trace relation" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a static HTML file
&lt;/h2&gt;

&lt;p&gt;This was the one design decision I didn't waver on. I didn't want a dashboard, a login, or a background service watching your repo. I wanted something you could generate, drop in a PR description, email to a teammate, or just keep around as a snapshot of "this is what the app looked like in March." A plain HTML file with the graph data embedded in it does all of that without asking anything of the person opening it.&lt;/p&gt;

&lt;p&gt;The tradeoff is that it's a snapshot, not a live view — regenerate it after a big refactor and it's current again, but it won't update itself while you work. That's a fair trade for something that requires zero setup on the receiving end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's there and what isn't
&lt;/h2&gt;

&lt;p&gt;Being upfront about the current state: pipe usage in templates (&lt;code&gt;{{ value | somePipe }}&lt;/code&gt;) isn't tracked as an edge yet, class-binding selectors (&lt;code&gt;[ngClass]&lt;/code&gt;, &lt;code&gt;[class.x]&lt;/code&gt;) aren't matched, and there's no Nx monorepo support — it expects a standard Angular CLI &lt;code&gt;tsconfig.app.json&lt;/code&gt;/&lt;code&gt;angular.json&lt;/code&gt; layout. Route parsing covers the common static patterns, not every dynamic route construction you could write. These are documented, not hidden, and they're the next things on the list.&lt;/p&gt;

&lt;p&gt;What is there: real Angular template AST parsing (not regex), signal-based &lt;code&gt;input()&lt;/code&gt;/&lt;code&gt;output()&lt;/code&gt;/&lt;code&gt;model()&lt;/code&gt; API extraction with aliases, the experimental &lt;code&gt;@Service()&lt;/code&gt; decorator alongside &lt;code&gt;@Injectable()&lt;/code&gt;, and a Playwright test suite covering the actual generated report in a real browser, not just unit tests against the analyzer.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ng-loom ./path/to/your-angular-app &lt;span class="nt"&gt;--out&lt;/span&gt; report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's on npm as &lt;a href="https://www.npmjs.com/package/ng-loom" rel="noopener noreferrer"&gt;&lt;code&gt;ng-loom&lt;/code&gt;&lt;/a&gt;, and the source is on GitHub at &lt;a href="https://github.com/xonaib/ng-loom" rel="noopener noreferrer"&gt;xonaib/ng-loom&lt;/a&gt;. If you run it against something interesting — or it chokes on your workspace layout — I'd genuinely like to hear about it. Open an issue, or just tell me what broke.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a form builder that lives inside your AI chat</title>
      <dc:creator>Zonaib Bokhari</dc:creator>
      <pubDate>Tue, 05 May 2026 10:06:20 +0000</pubDate>
      <link>https://dev.to/zonaibbokhari/i-built-a-form-builder-that-lives-inside-your-ai-chat-31gg</link>
      <guid>https://dev.to/zonaibbokhari/i-built-a-form-builder-that-lives-inside-your-ai-chat-31gg</guid>
      <description>&lt;p&gt;Context switching is the thing that kills me.&lt;/p&gt;

&lt;p&gt;I'm mid-conversation with Claude, figuring something out, and then I need to collect responses from a few people. So I open Typeform in another tab, build the form, copy the link, come back. By which point I've completely lost the thread of what I was thinking.&lt;/p&gt;

&lt;p&gt;So one weekend I just decided to fix it for myself. The result is MCP Forms — you describe a form to Claude, it builds it and hands you a live link, and you can read the responses back later. All in the same chat window.&lt;/p&gt;




&lt;h2&gt;
  
  
  Here's what it actually looks like
&lt;/h2&gt;

&lt;p&gt;You just talk to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a job application form. Ask for full name, email, and experience level 
(Junior / Mid / Senior). If they select Senior, show a field asking about 
their leadership experience.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude calls the tool, the form gets generated, and you get a shareable link back — or an inline preview card if you're in a supported client:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F446r6oy3y1fdlbzmwuph.gif" class="article-body-image-wrapper"&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.amazonaws.com%2Fuploads%2Farticles%2F446r6oy3y1fdlbzmwuph.gif" alt="Form creation demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The form itself is clean, works on mobile, has a light/dark toggle. The conditional logic works exactly as you'd expect — select Senior, the leadership field appears:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vrnhkwfanvge856kvi6.gif" class="article-body-image-wrapper"&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.amazonaws.com%2Fuploads%2Farticles%2F7vrnhkwfanvge856kvi6.gif" alt="Filling the form — conditional field appears when Senior is selected"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want to see who filled it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show me submissions for the job application form
Export as CSV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxonaib%2Fmcp-forms%2Fmain%2Fassets%2FAnimation3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxonaib%2Fmcp-forms%2Fmain%2Fassets%2FAnimation3.gif" alt="Viewing submissions and exporting to CSV"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it supports
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Field types:&lt;/strong&gt; text, email, number, textarea, dropdown (select), radio buttons, checkboxes, and date. Claude picks the right type from your description — ask for an email field and it generates &lt;code&gt;type: email&lt;/code&gt; with validation, ask for a yes/no question and it generates a radio group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional logic:&lt;/strong&gt; any field can have a &lt;code&gt;showIf&lt;/code&gt; rule that makes it appear only when another field has a specific value. Works across field types — radio, dropdown, whatever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dark theme:&lt;/strong&gt; forms default to light, but every form has a built-in theme toggle that respondents can use themselves. You can also set the default at creation time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a feedback form using the dark theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Updating forms:&lt;/strong&gt; this one I use a lot. You don't have to rebuild from scratch if something's wrong — just tell Claude what to change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add a phone number field to the job application form
Change the experience level options to Junior, Mid, Senior, Lead
Remove the cover letter field
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The form updates in place. Same URL, same submissions, just a different shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draft / Publish workflow:&lt;/strong&gt; forms start as drafts so you can preview before anyone fills them out. You publish explicitly when you're ready to share the link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email notifications:&lt;/strong&gt; get an email on every new submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a contact form and notify me at you@example.com on each submission
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Redirect after submit:&lt;/strong&gt; send respondents somewhere specific after they hit submit — a thank-you page, back to your site, anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create an application form and redirect to https://example.com/thanks after submission
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The bit I find genuinely cool about this
&lt;/h2&gt;

&lt;p&gt;Most MCP tools are wrappers around read operations. Search this, summarize that, fetch the other thing. What I liked about building this one is that the AI is actually creating something that persists — a real form at a real URL that other people can fill out, that collects real data.&lt;/p&gt;

&lt;p&gt;It sounds small but it's a different kind of thing. The AI isn't just helping you think, it's helping you build.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;p&gt;Stack is pretty simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Desktop / Claude.ai / Cursor / any MCP client
        │
        │  MCP (stdio or streamable-http)
        ▼
Python MCP Server
        │
        │  HTTP
        ▼
ASP.NET 9 API (on Railway)
        │
        ├── Claude API → JSON form schema
        ├── SQLite for storage
        └── Scriban templates → HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow that makes it work: when you describe a form, the API calls Claude with a prompt that returns a JSON schema — field types, labels, options, conditional rules. The form renderer takes that schema and generates HTML server-side using Scriban templates. Claude runs exactly once per form, at creation time. Loading the form page doesn't touch the AI API at all.&lt;/p&gt;

&lt;p&gt;Conditional logic lives in the schema as &lt;code&gt;showIf&lt;/code&gt; rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"leadership_experience"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"textarea"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Describe your leadership experience"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"showIf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"experience_level"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Senior"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a bit of vanilla JS on the form that watches trigger fields and shows/hides dependents. One thing I got wrong the first time — I was using &lt;code&gt;getElementById&lt;/code&gt; to find radio button groups, which doesn't work because radio inputs use &lt;code&gt;name&lt;/code&gt; not &lt;code&gt;id&lt;/code&gt;. Had to fall back to &lt;code&gt;querySelectorAll('[name="..."]')&lt;/code&gt;. Classic.&lt;/p&gt;

&lt;p&gt;On the security side: MCP endpoints are behind an API key, form view and submit are public (obviously — people need to fill out your forms), server-side validation mirrors the schema rules, rate limiting on submit, XSS handled by Scriban's auto-encoding. Nothing exotic, just the basics done properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  No install needed if you just want to try it
&lt;/h2&gt;

&lt;p&gt;The MCP server is hosted on Railway with streamable-http transport. Add it as a custom connector in Claude.ai:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mcp-forms-production-cdf7.up.railway.app/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Settings → Connectors → Add custom connector. Works the same way in ChatGPT, Cursor, Windsurf.&lt;/p&gt;




&lt;h2&gt;
  
  
  The webhook thing
&lt;/h2&gt;

&lt;p&gt;Every form can have a webhook URL. On submission, the payload gets POSTed there. If it's a Slack incoming webhook URL, MCP Forms formats the payload as a proper Slack message automatically. Anything else gets raw JSON.&lt;/p&gt;

&lt;p&gt;I'm pretty happy with this design decision — one feature, zero native integrations, but it unlocks Slack, Zapier, Make, n8n, and Google Sheets at once. You just tell Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a contact form and send submissions to my Slack at 
https://hooks.slack.com/services/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submissions land in Slack like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New submission: Contact Form
• name: Jane Smith
• email: jane@example.com
• message: Interested in your services!
Submitted at 2026-01-15T10:32:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Try it / grab the code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/xonaib/mcp-forms" rel="noopener noreferrer"&gt;github.com/xonaib/mcp-forms&lt;/a&gt;&lt;/strong&gt; — MIT licensed&lt;/p&gt;

&lt;p&gt;You'll need .NET 9, Python, uv, and an Anthropic API key. Self-hosting takes maybe 10 minutes following the README.&lt;/p&gt;

&lt;p&gt;A few things still on the list: Notion integration, file upload fields, and proper multi-tenant auth (right now it's single-tenant — one database, one API key). Happy to take PRs or ideas.&lt;/p&gt;

&lt;p&gt;Built this mostly on weekends and evening sessions between putting the kids to bed and falling asleep on the couch. If you try it and something's broken, open an issue — I'm actively working on it.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>claude</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I got tired of copy-pasting the same table code, so I built a library</title>
      <dc:creator>Zonaib Bokhari</dc:creator>
      <pubDate>Mon, 20 Apr 2026 08:56:21 +0000</pubDate>
      <link>https://dev.to/zonaibbokhari/i-got-tired-of-copy-pasting-the-same-table-code-so-i-built-a-library-2c3l</link>
      <guid>https://dev.to/zonaibbokhari/i-got-tired-of-copy-pasting-the-same-table-code-so-i-built-a-library-2c3l</guid>
      <description>&lt;p&gt;Every Angular project I've worked on has a table. Usually more than one. And every single time I end up writing the same setup — wire up &lt;code&gt;MatSort&lt;/code&gt;, wire up &lt;code&gt;MatPaginator&lt;/code&gt;, build a &lt;code&gt;SelectionModel&lt;/code&gt; for checkboxes, manage filter state somewhere, figure out export again from scratch.&lt;br&gt;
It's not hard, it's just tedious. And when you do it enough times across enough projects, slightly differently each time, you start to wonder why you haven't just extracted it.&lt;br&gt;
So I did. &lt;a href="https://www.npmjs.com/package/ngx-mat-simple-table" rel="noopener noreferrer"&gt;&lt;code&gt;ngx-mat-simple-table&lt;/code&gt;&lt;/a&gt; — an Angular Material table component that takes a column config and data, and handles the rest.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8lzkdbkok00ed0lwrar.png" class="article-body-image-wrapper"&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.amazonaws.com%2Fuploads%2Farticles%2Fo8lzkdbkok00ed0lwrar.png" alt="Screenshot of table from Deployed link" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;I wanted to go from this (the usual boilerplate situation) to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;simple-table&lt;/span&gt;
  &lt;span class="na"&gt;[tableColumns]=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt;
  &lt;span class="na"&gt;[dataSource]=&lt;/span&gt;&lt;span class="s"&gt;"rows"&lt;/span&gt;
  &lt;span class="na"&gt;[tableConfig]=&lt;/span&gt;&lt;span class="s"&gt;"config"&lt;/span&gt;
  &lt;span class="na"&gt;(sortChange)=&lt;/span&gt;&lt;span class="s"&gt;"onSort($event)"&lt;/span&gt;
  &lt;span class="na"&gt;(filterChange)=&lt;/span&gt;&lt;span class="s"&gt;"onFilter($event)"&lt;/span&gt;
  &lt;span class="na"&gt;(selectionChange)=&lt;/span&gt;&lt;span class="s"&gt;"onSelect($event)"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;st-export&lt;/span&gt; &lt;span class="na"&gt;filename=&lt;/span&gt;&lt;span class="s"&gt;"tasks"&lt;/span&gt; &lt;span class="na"&gt;format=&lt;/span&gt;&lt;span class="s"&gt;"xlsx"&lt;/span&gt; &lt;span class="na"&gt;[allDataProvider]=&lt;/span&gt;&lt;span class="s"&gt;"getAllForExport"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/simple-table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Column config is a plain array:&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;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ColumnDef&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;hasColumnFilters&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assignee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Assignee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hasColumnFilters&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="na"&gt;filterType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FilterType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DropDown&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;hasColumnFilters&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="na"&gt;filterType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FilterType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DropDown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;displayValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dueDate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Due Date&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;Fully paginated, sortable, filterable, exportable table. That's the whole host component.&lt;/p&gt;




&lt;h2&gt;
  
  
  Signals from the start
&lt;/h2&gt;

&lt;p&gt;I built this after Angular 17 shipped, so I went all-in on the signals API. No &lt;code&gt;@Input()&lt;/code&gt;, no &lt;code&gt;EventEmitter&lt;/code&gt;, no &lt;code&gt;ChangeDetectorRef&lt;/code&gt;. Everything is &lt;code&gt;input()&lt;/code&gt;, &lt;code&gt;output()&lt;/code&gt;, &lt;code&gt;computed()&lt;/code&gt;, &lt;code&gt;effect()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wasn't sure how it would feel at first but honestly it's made the component much easier to reason about. I've never once had to think about change detection. Would not go back.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Windows &lt;code&gt;file:&lt;/code&gt; reference trap
&lt;/h2&gt;

&lt;p&gt;This one annoyed me more than it should have.&lt;/p&gt;

&lt;p&gt;When developing a library locally you need the demo app to consume the built output. I used &lt;code&gt;"ngx-mat-simple-table": "file:./dist/ngx-mat-simple-table"&lt;/code&gt; in the root &lt;code&gt;package.json&lt;/code&gt;. On macOS this works fine. On Windows, &lt;code&gt;npm install&lt;/code&gt; with a &lt;code&gt;file:&lt;/code&gt; reference copies the files at install time — so running &lt;code&gt;build:lib:watch&lt;/code&gt; updates &lt;code&gt;dist/&lt;/code&gt; but &lt;code&gt;node_modules/&lt;/code&gt; stays completely stale. I kept seeing old code after rebuilds and couldn't figure out why for longer than I'd like to admit.&lt;/p&gt;

&lt;p&gt;The fix is &lt;code&gt;tsconfig.json&lt;/code&gt; paths instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ngx-mat-simple-table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./dist/ngx-mat-simple-table"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular's build system watches files resolved through &lt;code&gt;paths&lt;/code&gt;, so incremental rebuilds are picked up immediately. Should have just done this from the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  CDK drag-reorder was a puzzle
&lt;/h2&gt;

&lt;p&gt;Column drag-reorder uses Angular CDK. My first attempt put &lt;code&gt;cdkDropList&lt;/code&gt; and &lt;code&gt;cdkDrag&lt;/code&gt; on the same &lt;code&gt;&amp;lt;th&amp;gt;&lt;/code&gt; element. CDK silently reported &lt;code&gt;previousContainer === container&lt;/code&gt; on every drop, so the column order never actually changed. Body cells stayed out of sync with headers. No error, just nothing happening.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;&amp;lt;th&amp;gt;&lt;/code&gt; is the &lt;code&gt;CdkDropList&lt;/code&gt;, a wrapper &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; inside it carries &lt;code&gt;CdkDrag&lt;/code&gt;. Separate elements. Also — and this surprised me — Angular's &lt;code&gt;@for&lt;/code&gt; block doesn't work here. CDK needs to traverse the view tree to find connected drop lists, and &lt;code&gt;@for&lt;/code&gt; uses a different internal structure than &lt;code&gt;*ngFor&lt;/code&gt;. Switching to &lt;code&gt;*ngFor&lt;/code&gt; on the column blocks fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't install SheetJS without checking if it actually does what you need
&lt;/h2&gt;

&lt;p&gt;I needed styled Excel headers. I installed SheetJS (xlsx), the most popular option. Spent a while getting it set up, wrote the header styling code, tested it — headers were completely plain. No error, styling just silently had no effect.&lt;/p&gt;

&lt;p&gt;Turns out cell styles in SheetJS community edition are a Pro-only feature. It's in the docs if you look for it, but it's not exactly front and centre.&lt;/p&gt;

&lt;p&gt;Switched to &lt;a href="https://github.com/exceljs/exceljs" rel="noopener noreferrer"&gt;ExcelJS&lt;/a&gt; (MIT, actually free) and it worked immediately. The API is clean and it supports full cell styling. To match the exported header to the rendered grid I just read styles from the DOM at export time:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hostEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;th.mat-mdc-header-cell&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&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;cs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_cssColorToArgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// → ARGB hex for ExcelJS&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever theme or custom CSS the host applies, the Excel header automatically matches it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Export should export everything, not just what's on screen
&lt;/h2&gt;

&lt;p&gt;The first version of export grabbed whatever rows were rendered — so if you were on page 3 of 10, you'd export 10 rows. Obviously wrong in hindsight.&lt;/p&gt;

&lt;p&gt;Client-side mode was easy to fix: export &lt;code&gt;MatTableDataSource.filteredData&lt;/code&gt;, which has all filtered rows regardless of page.&lt;/p&gt;

&lt;p&gt;Server-side mode needed a different approach. The &lt;code&gt;&amp;lt;st-export&amp;gt;&lt;/code&gt; directive accepts an &lt;code&gt;allDataProvider&lt;/code&gt; callback — the host provides a function that fetches everything from the API without pagination params:&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;getAllForExport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;firstValueFrom&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;_http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TasksResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;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;/api/tasks&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="na"&gt;params&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="nf"&gt;activeFilterParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;st-export&lt;/span&gt; &lt;span class="na"&gt;filename=&lt;/span&gt;&lt;span class="s"&gt;"tasks"&lt;/span&gt; &lt;span class="na"&gt;[allDataProvider]=&lt;/span&gt;&lt;span class="s"&gt;"getAllForExport"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Active filters are forwarded so the export reflects exactly what the user sees — just without the page limit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vercel 404 after deploy
&lt;/h2&gt;

&lt;p&gt;After one release the demo started returning 404 on every route. Angular 17+'s esbuild builder outputs to &lt;code&gt;dist/&amp;lt;project&amp;gt;/browser/&lt;/code&gt; — Vercel was pointed at &lt;code&gt;dist/&amp;lt;project&amp;gt;/&lt;/code&gt; and finding no &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fixed with a &lt;code&gt;vercel.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build:lib &amp;amp;&amp;amp; npm install &amp;amp;&amp;amp; npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputDirectory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/Demo-table/browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rewrites"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/(.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/index.html"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rewrites&lt;/code&gt; rule matters too. Without it, refreshing any route other than &lt;code&gt;/&lt;/code&gt; returns 404 because Vercel looks for a file at that path instead of letting Angular's router handle it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/ngx-mat-simple-table" rel="noopener noreferrer"&gt;&lt;code&gt;ngx-mat-simple-table&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://ng-simple-table.vercel.app" rel="noopener noreferrer"&gt;ng-simple-table.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/xonaib/ng-simple-table" rel="noopener noreferrer"&gt;github.com/xonaib/ng-simple-table&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It has pagination, sorting, multi-select, dropdown filters, column chooser, drag-reorder, column resize, sticky columns, Excel export with full header styling, user settings persistence, and virtual scroll. Client-side and server-side data modes.&lt;/p&gt;

&lt;p&gt;If you're building data-heavy Angular apps, hopefully it saves you some of the boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in v1.2
&lt;/h2&gt;

&lt;p&gt;Since the original post the library has grown quite a bit. Here's what landed in v1.2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sticky columns&lt;/strong&gt; — &lt;code&gt;ColumnDef.sticky: 'left' | 'right'&lt;/code&gt; pins columns to either edge during horizontal scroll. Drag-reorder is automatically disabled for sticky columns so users can't accidentally break the layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt; — all colour tokens alias Angular Material 3 system tokens, so flipping &lt;code&gt;body { color-scheme: dark }&lt;/code&gt; adapts the entire table with zero extra CSS. Use the CSS &lt;code&gt;light-dark()&lt;/code&gt; function for any custom cell colours and they adapt too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cellClass&lt;/code&gt; callback&lt;/strong&gt; — return a CSS class from &lt;code&gt;(value, row)&lt;/code&gt; for conditional cell styling — colour-coded status badges, priority indicators — without needing a full custom template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fillContainer&lt;/code&gt; mode&lt;/strong&gt; — set &lt;code&gt;TableConfig.fillContainer: true&lt;/code&gt; and the table stretches to fill its parent height. Toolbar and paginator stay pinned; only the rows scroll.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22 CSS custom properties&lt;/strong&gt; — every visual surface now has a &lt;code&gt;--st-*&lt;/code&gt; token covering header, borders, row backgrounds, hover, sticky cells, cell text, scrollbar, filter popup, and column chooser. Override any of them on the element or any ancestor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State persistence fix&lt;/strong&gt; — new columns added after a saved state are now correctly appended rather than silently dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's new in v1.3
&lt;/h2&gt;

&lt;p&gt;v1.3 ships virtual scroll.&lt;/p&gt;

&lt;p&gt;Pagination works, but some UIs just want to scroll. Flip one flag and the paginator disappears and CDK virtual scroll takes over — only the rows in the viewport are in the DOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side&lt;/strong&gt; is a one-liner config change:&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;simple-table&lt;/span&gt;
  &lt;span class="na"&gt;[dataSource]=&lt;/span&gt;&lt;span class="s"&gt;"allRows"&lt;/span&gt;
  &lt;span class="na"&gt;[columns]=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt;
  &lt;span class="na"&gt;[config]=&lt;/span&gt;&lt;span class="s"&gt;"{ virtual: true, virtualRowHeight: 48 }"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass your full array. The component handles everything internally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side&lt;/strong&gt; is where it gets more interesting. Instead of fetching everything upfront, the table emits a &lt;code&gt;virtualRangeChange&lt;/code&gt; event as the user scrolls, telling you which rows are in view. You fetch just that window:&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;simple-table&lt;/span&gt;
  &lt;span class="na"&gt;[dataSource]=&lt;/span&gt;&lt;span class="s"&gt;"currentWindow"&lt;/span&gt;
  &lt;span class="na"&gt;[columns]=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt;
  &lt;span class="na"&gt;[config]=&lt;/span&gt;&lt;span class="s"&gt;"{ virtual: true, virtualRowHeight: 48 }"&lt;/span&gt;
  &lt;span class="na"&gt;[virtualOffset]=&lt;/span&gt;&lt;span class="s"&gt;"windowStart"&lt;/span&gt;
  &lt;span class="na"&gt;[totalLength]=&lt;/span&gt;&lt;span class="s"&gt;"totalRecords"&lt;/span&gt;
  &lt;span class="na"&gt;(virtualRangeChange)=&lt;/span&gt;&lt;span class="s"&gt;"onRangeChange($event)"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;onRangeChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;VirtualRange&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;windowStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&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;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/items?offset=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;currentWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&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;virtualOffset&lt;/code&gt; tells the table where in the full dataset your loaded window starts. &lt;code&gt;totalLength&lt;/code&gt; sizes the scroll track correctly so the scrollbar reflects the real dataset. You never load more than what's on screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing that took longer than expected:&lt;/strong&gt; CDK virtual scroll normally positions loaded content by applying &lt;code&gt;transform: translateY()&lt;/code&gt; to the content wrapper. That creates a CSS stacking context, which breaks &lt;code&gt;position: sticky&lt;/code&gt; on the table header — the header scrolls away with the content instead of staying fixed. The fix is to no-op that transform entirely and use &lt;code&gt;margin-top&lt;/code&gt; instead. Same visual positioning, no stacking context, sticky headers stay sticky. Not obvious until you hit it.&lt;/p&gt;

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