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.
npm install --save-dev hollows-uiImport the generated registry in your main entry:
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:
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.
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
| Input | Type | Default | Description |
|---|---|---|---|
| name | string | - | Unique identifier matching the generated definition |
| empty | boolean | false | Whether to show the empty state |
| loading | boolean | false | Show skeleton loading state |
| category | string | auto | Override the auto-detected category |
| copy | CopyOverride | - | Override generated copy |
| animate | boolean | true | Enable transition animations |
| cssClass | string | - | Additional CSS class for the host element |
Outputs:
| Output | Type | Description |
|---|---|---|
| action | EventEmitter<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.
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.
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.
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.
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.
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}