Angular

The Angular adapter provides the HollowComponent directive and HollowService for integrating auto-generated empty states into Angular applications with full dependency injection support.

Installation

Import from the hollows-ui/angular subpath. The adapter supports Angular 16+ and works with both NgModules and standalone components.

Terminal
bash
npm install --save-dev hollows-ui

Import the generated registry in your main entry:

src/main.ts
typescript
import './hollows/registry'import { bootstrapApplication } from '@angular/platform-browser'import { AppComponent } from './app/app.component'import { appConfig } from './app/app.config'bootstrapApplication(AppComponent, appConfig)

Module setup

If you use NgModules, import HollowsModule in your app module or feature module:

app.module.ts
typescript
1import { NgModule } from '@angular/core'2import { BrowserModule } from '@angular/platform-browser'3import { HollowsModule } from 'hollows-ui/angular'4import { AppComponent } from './app.component'56@NgModule({7  declarations: [AppComponent],8  imports: [9    BrowserModule,10    HollowsModule,  // Provides <hollow> component globally11  ],12  bootstrap: [AppComponent],13})14export class AppModule {}

Basic usage

Use the <hollow> component in your templates. It renders the auto-generated empty state when [empty] is true and projects your content when false.

inbox.component.ts
typescript
1import { Component, Input, Output, EventEmitter } from '@angular/core'23interface Message {4  id: string5  subject: string6  preview: string7}89@Component({10  selector: 'app-inbox',11  template: `12    <div class="inbox-container">13      <h2>Inbox</h2>14      <hollow15        name="user-inbox"16        [empty]="!loading && messages.length === 0"17        [loading]="loading"18        (action)="onCompose()"19      >20        <app-message-card21          *ngFor="let msg of messages; trackBy: trackById"22          [message]="msg"23        />24      </hollow>25    </div>26  `,27})28export class InboxComponent {29  @Input() messages: Message[] = []30  @Input() loading = false3132  onCompose() {33    this.router.navigate(['/compose'])34  }3536  trackById(index: number, item: Message) {37    return item.id38  }39}

Inputs & outputs

InputTypeDefaultDescription
namestring-Unique identifier matching the generated definition
emptybooleanfalseWhether to show the empty state
loadingbooleanfalseShow skeleton loading state
categorystringautoOverride the auto-detected category
copyCopyOverride-Override generated copy
animatebooleantrueEnable transition animations
cssClassstring-Additional CSS class for the host element

Outputs:

OutputTypeDescription
actionEventEmitter<void>Emitted when the CTA button is clicked

Template customization

Use Angular's content projection and template references to customize the empty state. The #hollowEmpty template receives the generated state as a context variable.

Custom empty state template
typescript
1@Component({2  selector: 'app-notifications',3  template: `4    <hollow5      name="notifications"6      [empty]="notifications.length === 0"7    >8      <!-- Normal content via content projection -->9      <app-notification-item10        *ngFor="let n of notifications"11        [notification]="n"12      />1314      <!-- Custom empty state template -->15      <ng-template #hollowEmpty let-state>16        <div class="flex flex-col items-center py-12">17          <app-bell-off-icon class="w-16 h-16 text-gray-400 mb-4" />18          <h3 class="text-lg font-semibold">{{ state.copy.headline }}</h3>19          <p class="text-gray-500 mt-2">{{ state.copy.description }}</p>20          <button21            class="mt-6 px-4 py-2 bg-amber-500 text-black rounded-lg"22            (click)="enableNotifications()"23          >24            {{ state.copy.cta.label }}25          </button>26        </div>27      </ng-template>2829      <!-- Custom illustration template -->30      <ng-template #hollowIllustration>31        <app-lottie-player32          src="/animations/empty-bell.json"33          [width]="200"34          [height]="180"35        />36      </ng-template>37    </hollow>38  `,39})40export class NotificationsComponent {41  notifications: Notification[] = []4243  enableNotifications() {44    // Handle enabling notifications45  }46}

HollowService

For programmatic access to hollow data, inject the HollowService. This is useful when you need the generated data outside of templates.

Using HollowService
typescript
1import { Component, inject } from '@angular/core'2import { HollowService } from 'hollows-ui/angular'34@Component({5  selector: 'app-dashboard',6  template: `7    <div *ngIf="emptyState" class="custom-empty">8      <h3>{{ emptyState.copy.headline }}</h3>9      <p>{{ emptyState.copy.description }}</p>10    </div>11  `,12})13export class DashboardComponent {14  private hollowService = inject(HollowService)1516  emptyState = this.hollowService.getState('analytics-panel')1718  // Get all registered hollows19  allHollows = this.hollowService.getAll()2021  // Check if a hollow is registered22  hasHollow = this.hollowService.has('analytics-panel')23}

Standalone components

With Angular standalone components, import HollowComponent directly in your component's imports array.

Standalone component
typescript
1import { Component, input, output } from '@angular/core'2import { HollowComponent } from 'hollows-ui/angular'34@Component({5  standalone: true,6  imports: [HollowComponent],7  selector: 'app-file-gallery',8  template: `9    <hollow10      name="file-gallery"11      [empty]="files().length === 0"12      [loading]="isLoading()"13      (action)="onUpload.emit()"14    >15      <div class="grid grid-cols-3 gap-4">16        @for (file of files(); track file.id) {17          <app-file-card [file]="file" />18        }19      </div>20    </hollow>21  `,22})23export class FileGalleryComponent {24  files = input<File[]>([])25  isLoading = input(false)26  onUpload = output<void>()27}

Signals

The hollow component works seamlessly with Angular signals. Use input() and computed() for reactive empty state conditions.

Signal-based empty state
typescript
1import { Component, computed, signal } from '@angular/core'2import { HollowComponent } from 'hollows-ui/angular'34@Component({5  standalone: true,6  imports: [HollowComponent],7  selector: 'app-search',8  template: `9    <input10      [value]="query()"11      (input)="query.set($event.target.value)"12      placeholder="Search..."13    />14    <hollow15      name="search-results"16      [empty]="isSearchEmpty()"17      [copy]="{ headline: 'No results for \"' + query() + '\"' }"18      (action)="query.set('')"19    >20      @for (result of filteredResults(); track result.id) {21        <app-result-card [result]="result" />22      }23    </hollow>24  `,25})26export class SearchComponent {27  query = signal('')28  results = signal<Result[]>([])2930  filteredResults = computed(() =>31    this.results().filter((r) =>32      r.title.toLowerCase().includes(this.query().toLowerCase())33    )34  )3536  isSearchEmpty = computed(() =>37    this.query().length > 0 && this.filteredResults().length === 038  )39}

Reactive forms

Combine hollows with Angular reactive forms to show empty states based on form values, such as search inputs or filter controls.

Reactive forms integration
typescript
1import { Component, inject } from '@angular/core'2import { FormControl, ReactiveFormsModule } from '@angular/forms'3import { HollowComponent } from 'hollows-ui/angular'4import { debounceTime, switchMap, map } from 'rxjs'5import { toSignal } from '@angular/core/rxjs-interop'6import { SearchService } from './search.service'78@Component({9  standalone: true,10  imports: [ReactiveFormsModule, HollowComponent],11  selector: 'app-search-page',12  template: `13    <input [formControl]="searchControl" placeholder="Search..." />14    <hollow15      name="search-results"16      [empty]="results().length === 0 && searchControl.value.length > 0"17      [loading]="isLoading()"18      (action)="searchControl.reset()"19    >20      @for (item of results(); track item.id) {21        <app-search-result [item]="item" />22      }23    </hollow>24  `,25})26export class SearchPageComponent {27  private searchService = inject(SearchService)2829  searchControl = new FormControl('')3031  private searchResults$ = this.searchControl.valueChanges.pipe(32    debounceTime(300),33    switchMap((query) => this.searchService.search(query ?? '')),34  )3536  results = toSignal(this.searchResults$.pipe(map(r => r.items)), {37    initialValue: [],38  })3940  isLoading = toSignal(this.searchResults$.pipe(map(r => r.loading)), {41    initialValue: false,42  })43}