You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Signal-first CRUD in Angular 22: one list page for every table
Admin panels repeat themselves. Every table gets a list page, a create form, an edit form, a delete button, loading and error states. With Signals and the native resource() API, Angular now has what it takes to write that layer once and reuse it for every table.
This article shows how, using Pidrang, an open-source headless admin framework for Angular that I maintain. It is version 0.1.1 and pre-1.0, so the API may still change. The backend here is Supabase, but the pattern does not depend on it.
What we build
A typed Resource that describes a products table.
One list component that serves every table, driven by a route parameter.
Per-row edit and delete actions.
A create form that only sends the fields the user changed.
Prerequisites: an Angular 22 app with standalone APIs and the router, a Supabase project with a table that has an id primary key, and its URL and publishable key (formerly anon).
1. Install
npm install @pidrang/core @pidrang/supabase @pidrang/material @supabase/supabase-js
ng add @angular/material
@pidrang/material is one of two UI layers. @pidrang/tailwind offers the same components styled with Tailwind utility classes.
name matches the table. Each field has a type (text, number, boolean, date, timestamp, time, numeric), and the UI renders columns and inputs from it. readOnly keeps generated columns out of forms. date, timestamp, time and numeric stay plain strings end to end, so a high-precision number or a zoned time is never rounded through Number or Date.
provideSupabaseDataProvider returns a plain object implementing a five-method contract: getList, getOne, create, update, delete. providePidrang is the only place that touches Angular DI. withComponentInputBinding() carries the :resource segment of the next step to the page's input; without it the page renders blank. Use the publishable key, never the secret key: the app runs in the browser.
4. One list page for every table
This is where resource() earns its place. injectResourceList wraps DataProvider.getList in Angular's resource(), so value(), isLoading(), error() and reload() are signals. The resource name can itself be a function, so when the route parameter changes, the list reloads.
Filters are a typed union, one member per operator, so a value that does not fit its operator does not compile.
5. Row actions
ResourceTable projects an #actions template as a trailing column. Add RouterLink to the page's imports, and injectDataProvider from @pidrang/core and signal from @angular/core to its imports:
privatereadonlydataProvider=injectDataProvider();protectedreadonlydeleteError=signal<string|null>(null);protectedasyncremove(id: string): Promise<void>{if(!confirm('Delete this record?')){return;}try{awaitthis.dataProvider.delete(this.resource(),{ id });this.deleteError.set(null);this.list.reload();}catch(error){this.deleteError.set(errorinstanceofError ? error.message : String(error));}}
Errors thrown by the Supabase provider are PidrangErrors with a category (unauthenticated, forbidden, not-found, unknown), so the page can ask the user to sign in instead of showing a raw PostgREST message.
6. A form that sends only what changed
ResourceForm is built on Angular Signal Forms. It never mutates the value you pass in, and its save output only contains the fields the user touched, so the database default or NULL applies to everything else.
On edit, save does not emit at all when nothing changed, since there is nothing to PATCH.
Authorization stays in the database
Pidrang performs no authorization. With Supabase, the Row Level Security policies on your tables decide who reads and writes. Enable RLS on every exposed table before deploying: the publishable key ships to every browser.
Skip the typing: generate it
The same resources.ts and generic list, create, show and edit pages can be generated from your schema:
npx pidrang init
It reads your Supabase schema and asks which tables and actions you want. It offers to install the packages, registers Pidrang in app.config.ts, adds the routes to app.routes.ts, writes the environment files when the app has none, and generates a sign-in page. It never overwrites a file: if one already exists with other content, it stops before writing anything and lists the paths.
Wrap-up
resource() plus a reactive resource name gives one list component for every table.
A typed field list drives both the table and the form.
The data layer is a five-method contract, so Supabase is a choice, not a requirement.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Signal-first CRUD in Angular 22: one list page for every table
Admin panels repeat themselves. Every table gets a list page, a create form, an edit form, a delete button, loading and error states. With Signals and the native
resource()API, Angular now has what it takes to write that layer once and reuse it for every table.This article shows how, using Pidrang, an open-source headless admin framework for Angular that I maintain. It is version 0.1.1 and pre-1.0, so the API may still change. The backend here is Supabase, but the pattern does not depend on it.
What we build
Resourcethat describes aproductstable.Prerequisites: an Angular 22 app with standalone APIs and the router, a Supabase project with a table that has an
idprimary key, and its URL and publishable key (formerly anon).1. Install
@pidrang/materialis one of two UI layers.@pidrang/tailwindoffers the same components styled with Tailwind utility classes.2. Describe the table once
namematches the table. Each field has a type (text,number,boolean,date,timestamp,time,numeric), and the UI renders columns and inputs from it.readOnlykeeps generated columns out of forms.date,timestamp,timeandnumericstay plain strings end to end, so a high-precision number or a zoned time is never rounded throughNumberorDate.3. Register the providers
provideSupabaseDataProviderreturns a plain object implementing a five-method contract:getList,getOne,create,update,delete.providePidrangis the only place that touches Angular DI.withComponentInputBinding()carries the:resourcesegment of the next step to the page's input; without it the page renders blank. Use the publishable key, never the secret key: the app runs in the browser.4. One list page for every table
This is where
resource()earns its place.injectResourceListwrapsDataProvider.getListin Angular'sresource(), sovalue(),isLoading(),error()andreload()are signals. The resource name can itself be a function, so when the route parameter changes, the list reloads.Route it with a
:resourcesegment:Open
/products: one column per field. Add a secondResourceand/ordersworks with the same component.Paging, sorting and filtering are a second, reactive argument. Return
undefinedfrom it to skip loading.Filters are a typed union, one member per operator, so a value that does not fit its operator does not compile.
5. Row actions
ResourceTableprojects an#actionstemplate as a trailing column. AddRouterLinkto the page'simports, andinjectDataProviderfrom@pidrang/coreandsignalfrom@angular/coreto its imports:Errors thrown by the Supabase provider are
PidrangErrors with acategory(unauthenticated,forbidden,not-found,unknown), so the page can ask the user to sign in instead of showing a raw PostgREST message.6. A form that sends only what changed
ResourceFormis built on Angular Signal Forms. It never mutates the value you pass in, and itssaveoutput only contains the fields the user touched, so the database default orNULLapplies to everything else.On
edit,savedoes not emit at all when nothing changed, since there is nothing toPATCH.Authorization stays in the database
Pidrang performs no authorization. With Supabase, the Row Level Security policies on your tables decide who reads and writes. Enable RLS on every exposed table before deploying: the publishable key ships to every browser.
Skip the typing: generate it
The same
resources.tsand generic list, create, show and edit pages can be generated from your schema:It reads your Supabase schema and asks which tables and actions you want. It offers to install the packages, registers Pidrang in
app.config.ts, adds the routes toapp.routes.ts, writes the environment files when the app has none, and generates a sign-in page. It never overwrites a file: if one already exists with other content, it stops before writing anything and lists the paths.Wrap-up
resource()plus a reactive resource name gives one list component for every table.Try it with the getting started guide, or look at the repository. Feedback and issues are welcome.
All reactions