# State Container

Use `ui-state-container` when a view needs a consistent state machine around async data. It keeps `loading`, `empty`, `error`, and `content` rendering in one place, which is usually cleaner than scattering conditionals across the page.

## Import
```ts
import { StateContainerComponent, initialState, loadingState, loadedState, errorState, type State } from 'ui';
```

## Initial, loading, empty, loaded, and error
```ts
import { Component } from '@angular/core';
import {
  StateContainerComponent,
  type State,
  initialState,
  loadedState,
  loadingState,
  errorState,
} from 'ui';

interface UserCard {
  id: number;
  name: string;
  role: string;
}

@Component({
  selector: 'app-state-container-basic-cycle-demo',
  standalone: true,
  imports: [StateContainerComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem;width:100%;max-width:58rem"
    >
      @for (preset of presets; track preset.label) {
        <div
          style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
        >
          <p
            style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
          >
            {{ preset.label }}
          </p>

          <ui-state-container
            [state]="preset.state"
            [showEmptyOnInitial]="true"
            loadingTitle="Loading users..."
            loadingDescription="Fetching the latest people data."
            emptyTitle="No users yet"
            emptyDescription="Invite your first teammate to populate this workspace."
            emptyIcon="people_add"
            errorTitle="Could not load users"
          >
            <div style="display:grid;gap:0.5rem">
              @for (user of preset.state.data ?? []; track user.id) {
                <div
                  style="padding:0.75rem 0.875rem;border-radius:0.75rem;background:var(--color-neutral-background2-rest)"
                >
                  <strong>{{ user.name }}</strong>
                  <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
                    {{ user.role }}
                  </div>
                </div>
              }
            </div>
          </ui-state-container>
        </div>
      }
    </div>
  `,
})
export class StateContainerBasicCycleDemoComponent {
  private readonly users: UserCard[] = [
    { id: 1, name: 'Anna Kowalska', role: 'Administrator' },
    { id: 2, name: 'Piotr Nowak', role: 'Project Manager' },
  ];

  protected readonly presets: Array<{ label: string; state: State<UserCard[]> }> = [
    { label: 'Initial', state: initialState<UserCard[]>() },
    { label: 'Loading', state: loadingState(initialState<UserCard[]>()) },
    { label: 'Empty', state: loadedState<UserCard[]>([]) },
    { label: 'Loaded', state: loadedState(this.users) },
    { label: 'Error', state: errorState<UserCard[]>('Unable to load people data.') },
  ];
}
```

## Empty-on-initial behavior
```ts
import { Component } from '@angular/core';
import { StateContainerComponent, initialState } from 'ui';

@Component({
  selector: 'app-state-container-empty-initial-demo',
  standalone: true,
  imports: [StateContainerComponent],
  template: `
    <div
      style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;width:100%;max-width:54rem"
    >
      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Initial hidden
        </p>
        <ui-state-container
          [state]="state"
          [showEmptyOnInitial]="false"
          emptyTitle="No records yet"
          emptyDescription="This will only show after the state resolves to empty."
        />
      </div>

      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Initial shown as empty
        </p>
        <ui-state-container
          [state]="state"
          [showEmptyOnInitial]="true"
          emptyTitle="Start by adding a record"
          emptyDescription="Useful for first-run experiences where an initial blank should be actionable."
          emptyIcon="document_add"
        />
      </div>
    </div>
  `,
})
export class StateContainerEmptyInitialDemoComponent {
  protected readonly state = initialState<any[]>();
}
```

## Overlay loading on existing content
```ts
import { Component, signal } from '@angular/core';
import { StateContainerComponent, type State, loadedState, loadingState } from 'ui';

interface SummaryCard {
  id: number;
  label: string;
  value: string;
}

@Component({
  selector: 'app-state-container-overlay-demo',
  standalone: true,
  imports: [StateContainerComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:42rem">
      <div style="display:flex;justify-content:flex-start">
        <button
          type="button"
          style="padding:0.5rem 0.875rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.75rem;background:var(--color-neutral-background-rest);cursor:pointer"
          (click)="toggleRefresh()"
        >
          {{ refreshing() ? 'Stop refresh' : 'Show refresh overlay' }}
        </button>
      </div>

      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-state-container
          [state]="state()"
          [loadingOverlay]="true"
          [loadingBlurContent]="true"
          loadingTitle="Refreshing summary"
          loadingDescription="Updating the latest metrics and activity."
        >
          <div
            style="display:grid;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));gap:0.75rem"
          >
            @for (item of data; track item.id) {
              <div
                style="padding:0.875rem 1rem;border-radius:0.875rem;background:var(--color-neutral-background2-rest)"
              >
                <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
                  {{ item.label }}
                </div>
                <strong style="font-size:1.125rem">{{ item.value }}</strong>
              </div>
            }
          </div>
        </ui-state-container>
      </div>
    </div>
  `,
})
export class StateContainerOverlayDemoComponent {
  protected readonly data: SummaryCard[] = [
    { id: 1, label: 'Active projects', value: '12' },
    { id: 2, label: 'Pending reviews', value: '4' },
    { id: 3, label: 'Failed jobs', value: '1' },
  ];

  protected readonly refreshing = signal(true);
  protected readonly state = signal<State<SummaryCard[]>>(loadingState(loadedState(this.data)));

  protected toggleRefresh(): void {
    this.refreshing.update(value => !value);
    this.state.set(
      this.refreshing() ? loadingState(loadedState(this.data)) : loadedState(this.data),
    );
  }
}
```

## Custom loading, empty, and data templates
```ts
import { Component, signal } from '@angular/core';
import { StateContainerComponent, type State, initialState, loadedState, loadingState } from 'ui';

@Component({
  selector: 'app-state-container-custom-templates-demo',
  standalone: true,
  imports: [StateContainerComponent],
  template: `
    <div
      style="width:100%;max-width:38rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <ui-state-container [state]="state()" [showEmptyOnInitial]="true">
        <ng-template #loadingContent>
          <div style="padding:2rem;text-align:center">
            <div style="margin-bottom:0.75rem;font-weight:600">Syncing records...</div>
            <div style="font-size:0.875rem;color:var(--color-neutral-foreground2-rest)">
              Step 2 of 3
            </div>
          </div>
        </ng-template>

        <ng-template #emptyContent>
          <div style="padding:2rem;text-align:center">
            <div style="margin-bottom:0.5rem;font-weight:600">All caught up</div>
            <div style="color:var(--color-neutral-foreground2-rest)">
              There are no new records to review.
            </div>
          </div>
        </ng-template>

        <ng-template #dataState let-data>
          <div style="padding:1rem;display:grid;gap:0.75rem">
            <div style="font-weight:600">Data loaded successfully</div>
            <div style="font-size:0.875rem;color:var(--color-neutral-foreground2-rest)">
              Records: {{ data?.length ?? 0 }}
            </div>
          </div>
        </ng-template>
      </ui-state-container>
    </div>
  `,
})
export class StateContainerCustomTemplatesDemoComponent {
  protected readonly state = signal<State<string[]>>(loadingState(initialState<string[]>()));

  constructor() {
    setTimeout(() => {
      this.state.set(loadedState<string[]>([]));
      setTimeout(() => {
        this.state.set(loadedState(['Record A', 'Record B']));
      }, 1200);
    }, 1200);
  }
}
```

## Embedded list or panel layout
```ts
import { Component } from '@angular/core';
import {
  StateContainerComponent,
  errorState,
  type State,
  TextComponent,
  ButtonComponent,
} from 'ui';

interface ActivityItem {
  id: number;
  title: string;
}

@Component({
  selector: 'app-state-container-list-layout-demo',
  standalone: true,
  imports: [ButtonComponent, StateContainerComponent, TextComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:60rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <div
        style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:0.75rem"
      >
        <ui-text placeholder="Search activity..." style="width:16rem" />
        <ui-button variant="secondary" appearance="outline">Filter</ui-button>
      </div>

      <div
        style="display:flex;align-items:center;justify-content:center;min-height:18rem;padding:1.5rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-state-container
          [state]="state"
          errorTitle="Could not load activity"
          errorIcon="history"
          [errorPrimaryAction]="retryAction"
        >
          <div style="display:grid;gap:0.75rem;width:100%">
            @for (item of state.data ?? []; track item.id) {
              <div
                style="padding:0.875rem;border-radius:0.75rem;background:var(--color-neutral-background-rest)"
              >
                {{ item.title }}
              </div>
            }
          </div>
        </ui-state-container>
      </div>
    </div>
  `,
})
export class StateContainerListLayoutDemoComponent {
  protected readonly state: State<ActivityItem[]> = errorState<ActivityItem[]>(
    'The activity feed could not be refreshed.',
  );

  protected readonly retryAction = {
    label: 'Retry',
    variant: 'primary' as const,
    icon: 'arrow_sync' as const,
    action: () => {},
  };
}
```

## Data template without wrapper conditionals
```ts
import { Component } from '@angular/core';
import { StateContainerComponent, loadedState, type State } from 'ui';

interface Metric {
  id: number;
  label: string;
  value: string;
}

@Component({
  selector: 'app-state-container-data-template-demo',
  standalone: true,
  imports: [StateContainerComponent],
  template: `
    <div
      style="width:100%;max-width:42rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <ui-state-container [state]="state">
        <ng-template #dataState let-data>
          <div
            style="display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:0.75rem"
          >
            @for (item of data ?? []; track item.id) {
              <div
                style="padding:0.875rem 1rem;border-radius:0.875rem;background:var(--color-neutral-background2-rest)"
              >
                <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
                  {{ item.label }}
                </div>
                <strong style="font-size:1.125rem">{{ item.value }}</strong>
              </div>
            }
          </div>
        </ng-template>
      </ui-state-container>
    </div>
  `,
})
export class StateContainerDataTemplateDemoComponent {
  protected readonly state: State<Metric[]> = loadedState<Metric[]>([
    { id: 1, label: 'Open tasks', value: '24' },
    { id: 2, label: 'Blocked items', value: '3' },
    { id: 3, label: 'Approvals', value: '7' },
  ]);
}
```

## Accessibility

### State clarity
The visible branch should make it clear whether the view is loading, empty, failed, or ready. Titles and descriptions passed to the built-in states should be specific enough to explain what is happening.

### Actionable recovery
When the container renders empty or error actions, those actions should lead to meaningful next steps.

| Branch | Guidance |
| --- | --- |
| Loading | Explain what is being loaded when the wait is noticeable. |
| Empty | Offer a next step only when one is genuinely available. |
| Error | Prefer direct recovery such as retrying or navigating to help. |

### Custom templates
If you override loading, empty, error, or data templates, preserve the same clarity and keyboard accessibility that the built-in state components provide.
