CngxAsyncContainer
projects/ui/feedback/async-container/async-container.ts
Import#
import { CngxAsyncContainer } from '@cngx/ui/feedback'
Description#
Async container molecule - coordinates all feedback states for data loading.
Projects four named templates and switches between them based on the
CngxAsyncState lifecycle. Includes a built-in refresh indicator (bar)
and ARIA state announcements.
<cngx-async-container [state]="residents">
<ng-template cngxAsyncSkeleton>
@for (i of [1,2,3]; track i) { <div class="skeleton-card"></div> }
</ng-template>
<ng-template cngxAsyncContent let-data>
@for (r of data; track r.id) { <app-card [resident]="r" /> }
</ng-template>
<ng-template cngxAsyncEmpty>
<cngx-empty-state title="No residents" />
</ng-template>
<ng-template cngxAsyncError let-err>
<cngx-alert severity="error">{{ err }}</cngx-alert>
</ng-template>
</cngx-async-container>Metadata#
Host#
Relationships
Index#
Inputs#
string | undefinedToast message on success. If set, fires a toast via CngxToaster.
HostBindings#
| Binding | Expression |
|---|---|
[attr.aria-busy] | state().isBusy() || null |
[attr.aria-label] | ariaLabel() || null |
Default visuals for CngxAsyncContainer. The host carries
.cngx-async-container but stays display: contents - the
container delegates rendering to projected skeleton / content /
empty / error templates. This stylesheet owns just two children:
the absolutely-positioned refresh indicator and the sr-only
announcement region.
Slots
.cngx-async-container__refresh- absolutely-positioned overlay anchored to the top edge, z-ordered above content (default5) but below dialog / popover surfaces.cngx-async-container__sr-only- visually-hidden live-region announcement target
Layout
<number>5Stacking-context order of the refresh indicator overlay. Defaults to 5 so it sits above the content stream but below dialog / popover surfaces.
Data flow
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { injectAsyncState } from '@cngx/common/data';
import {
CngxAsyncContainer,
CngxAsyncContentTpl,
CngxAsyncEmptyTpl,
CngxAsyncErrorTpl,
CngxAsyncSkeletonTpl,
} from '@cngx/ui/feedback';
/**
* Async-container data flow — one signal, four view states.
*
* `injectAsyncState` wraps a simulated HTTP call that re-fires whenever
* a tracked signal (the filter) changes. The returned `ReactiveAsyncState`
* is fed into `<cngx-async-container>` via `[state]`, and four projected
* templates (`cngxAsyncSkeleton`, `cngxAsyncContent`, `cngxAsyncEmpty`,
* `cngxAsyncError`) render the appropriate view — no manual `@if` chains.
* The first load shows the skeleton; subsequent refetches show the existing
* content plus a top-bar refresh indicator. Empty results route to the
* empty template; errors to the error template with the thrown value.
*/
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CngxAsyncContainer,
CngxAsyncSkeletonTpl,
CngxAsyncContentTpl,
CngxAsyncEmptyTpl,
CngxAsyncErrorTpl,
],
template: `
<p style="margin: 0 0 12px; opacity: 0.8; font-size: 0.875rem">
Type into the filter — <code>injectAsyncState</code> re-fires the
query, and <code><cngx-async-container></code> switches between
skeleton, content, empty, and error views from a single
<code>[state]</code> input.
</p>
<label style="display: flex; flex-direction: column; gap: 4px; max-width: 320px">
<span>Filter names</span>
<input
type="text"
[value]="filter()"
(input)="filter.set($any($event.target).value)"
placeholder="type to filter…"
/>
</label>
<cngx-async-container [state]="people" aria-label="People list">
<ng-template cngxAsyncSkeleton>
<p style="opacity: 0.6">Loading…</p>
</ng-template>
<ng-template cngxAsyncContent let-items>
<ul>
@for (item of items; track item) {
<li>{{ item }}</li>
}
</ul>
</ng-template>
<ng-template cngxAsyncEmpty>
<p>No results.</p>
</ng-template>
<ng-template cngxAsyncError let-err>
<p style="color: #b00020">Error: {{ err }}</p>
</ng-template>
</cngx-async-container>
`,
})
export class DataFlowExample {
protected readonly filter = signal('');
protected readonly people = injectAsyncState<string[]>(() => {
const query = this.filter().toLowerCase();
return new Promise<string[]>((resolve) =>
setTimeout(() => {
const all = ['Alice', 'Bob', 'Charlie', 'Diana'];
resolve(query ? all.filter((n) => n.toLowerCase().includes(query)) : all);
}, 1200),
);
});
}
export { DataFlowExample as AppComponent };