or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

application-management.mdchange-detection.mdcomponent-system.mddependency-injection.mdindex.mdmodern-authoring.mdresource-api.mdrxjs-interop.mdtesting.mdutilities-helpers.md
tile.json

resource-api.mddocs/

Resource API (Experimental)

Angular's Resource API provides a declarative way to manage asynchronous data loading with built-in loading states, error handling, and server-side rendering support. Resources offer a more structured approach to data fetching compared to manual observable management.

Capabilities

Core Resource Creation

Create resources for managing asynchronous data with automatic state management.

/**
 * Creates a resource with a loader function
 * @param loader - Function that returns data or Promise/Observable
 * @param options - Optional configuration
 * @returns Resource instance
 */
function resource<T>(
  loader: ResourceLoader<T>,
  options?: ResourceOptions
): WritableResource<T>;

/**
 * Creates a resource with request/loader configuration
 * @param config - Resource configuration object
 * @returns Resource instance
 */
function resource<T, R>(config: {
  request: () => R;
  loader: ResourceLoader<T, R>;
}): WritableResource<T>;

/**
 * Function type for resource loaders
 */
type ResourceLoader<T, R = unknown> = (request: R) => T | Promise<T> | Observable<T>;

/**
 * Configuration options for resources
 */
interface ResourceOptions {
  /** Injector for dependency injection context */
  injector?: Injector;
  /** Whether to load the resource on creation */
  manualLoading?: boolean;
  /** Equal function for comparing request values */
  equal?: (a: unknown, b: unknown) => boolean;
}

Resource Interfaces

Core interfaces for working with resources and their state.

/**
 * Read-only resource interface
 */
interface Resource<T> {
  /** Current resource value */
  value(): T | undefined;
  /** Current resource status */
  status(): ResourceStatus;
  /** Current error if any */
  error(): unknown;
  /** Whether resource is currently loading */
  isLoading(): boolean;
  /** Whether resource has a value */
  hasValue(): boolean;
}

/**
 * Writable resource interface with reload capability
 */
interface WritableResource<T> extends Resource<T> {
  /** Reload the resource */
  reload(): void;
  /** Set resource to loading state */
  set loading(): void;
  /** Set resource value directly */
  set(value: T): void;
  /** Update resource value using function */
  update(updateFn: (current: T | undefined) => T): void;
  /** Destroy the resource and cleanup subscriptions */
  destroy(): void;
}

/**
 * Resource reference for advanced use cases
 */
interface ResourceRef<T> extends WritableResource<T> {
  /** Original request used to create resource */
  request(): unknown;
  /** Resource loader function */
  loader(): ResourceLoader<T>;
}

/**
 * Resource status enumeration
 */
type ResourceStatus = 'idle' | 'loading' | 'resolved' | 'error' | 'local';

Resource State Management

Utilities for managing resource state and lifecycle.

/**
 * Check if a value is a resource
 * @param value - Value to check
 * @returns True if value is a resource
 */
function isResource(value: unknown): value is Resource<unknown>;

/**
 * Create a local resource with immediate value
 * @param value - Initial value for the resource
 * @returns Resource with local status
 */
function localResource<T>(value: T): WritableResource<T>;

/**
 * Transform a resource value using a mapping function
 * @param source - Source resource to transform
 * @param transform - Transformation function
 * @returns New resource with transformed values
 */
function mapResource<T, U>(
  source: Resource<T>,
  transform: (value: T) => U
): Resource<U>;

/**
 * Combine multiple resources into a single resource
 * @param resources - Array of resources to combine
 * @returns Resource containing array of all values
 */
function combineResources<T extends readonly Resource<any>[]>(
  resources: T
): Resource<{[K in keyof T]: T[K] extends Resource<infer U> ? U : never}>;

Server-Side Rendering Support

Utilities for managing resources in server-side rendering contexts.

/**
 * Preload resource data during SSR
 * @param resource - Resource to preload
 * @returns Promise that resolves when resource is loaded
 */
function preloadResource<T>(resource: Resource<T>): Promise<T>;

/**
 * Mark resource as server-only (won't execute on client)
 * @param resource - Resource to mark as server-only
 * @returns Modified resource configuration
 */
function serverOnlyResource<T>(resource: WritableResource<T>): WritableResource<T>;

/**
 * Resource hydration options for client-side
 */
interface ResourceHydrationOptions {
  /** Whether to revalidate data on client hydration */
  revalidate?: boolean;
  /** Timeout for server-side resource loading */
  timeout?: number;
}

Usage Examples

Basic Resource Usage

import { Component, resource } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-list',
  template: `
    <div>
      <h3>User List</h3>
      
      <div [ngSwitch]="usersResource.status()">
        <div *ngSwitchCase="'loading'">
          <p>Loading users...</p>
        </div>
        
        <div *ngSwitchCase="'error'">
          <p>Error: {{usersResource.error()}}</p>
          <button (click)="usersResource.reload()">Retry</button>
        </div>
        
        <div *ngSwitchCase="'resolved'">
          <ul>
            <li *ngFor="let user of usersResource.value()">
              {{user.name}} - {{user.email}}
            </li>
          </ul>
          <button (click)="usersResource.reload()">Refresh</button>
        </div>
      </div>
    </div>
  `
})
export class UserListComponent {
  private http = inject(HttpClient);
  
  // Resource that automatically loads user data
  usersResource = resource<User[]>(() =>
    this.http.get<User[]>('/api/users')
  );
}

Resource with Dynamic Requests

import { Component, signal, resource } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface UserDetails {
  id: number;
  name: string;
  email: string;
  profile: {
    bio: string;
    avatar: string;
    joinDate: string;
  };
}

@Component({
  selector: 'app-user-details',
  template: `
    <div>
      <h3>User Details</h3>
      
      <select (change)="selectUser($event)">
        <option value="">Select a user</option>
        <option value="1">Alice</option>
        <option value="2">Bob</option>
        <option value="3">Charlie</option>
      </select>
      
      <div *ngIf="selectedUserId()" [ngSwitch]="userDetailsResource.status()">
        <div *ngSwitchCase="'loading'">
          <p>Loading user details...</p>
        </div>
        
        <div *ngSwitchCase="'error'">
          <p>Failed to load user details</p>
          <button (click)="userDetailsResource.reload()">Retry</button>
        </div>
        
        <div *ngSwitchCase="'resolved'">
          <div class="user-profile">
            <img [src]="userDetailsResource.value()?.profile.avatar" 
                 [alt]="userDetailsResource.value()?.name">
            <h4>{{userDetailsResource.value()?.name}}</h4>
            <p>{{userDetailsResource.value()?.email}}</p>
            <p>{{userDetailsResource.value()?.profile.bio}}</p>
            <p>Joined: {{userDetailsResource.value()?.profile.joinDate | date}}</p>
          </div>
        </div>
      </div>
    </div>
  `
})
export class UserDetailsComponent {
  private http = inject(HttpClient);
  
  // Signal for selected user ID
  selectedUserId = signal<string | null>(null);
  
  // Resource that reloads when selectedUserId changes
  userDetailsResource = resource<UserDetails, string>({
    request: () => this.selectedUserId(),
    loader: (userId) => {
      if (!userId) {
        throw new Error('No user selected');
      }
      return this.http.get<UserDetails>(`/api/users/${userId}`);
    }
  });
  
  selectUser(event: Event): void {
    const target = event.target as HTMLSelectElement;
    this.selectedUserId.set(target.value || null);
  }
}

Resource Transformation and Combination

import { Component, resource, computed, mapResource, combineResources } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface RawWeatherData {
  temperature_celsius: number;
  humidity_percent: number;
  conditions: string;
}

interface ProcessedWeatherData {
  temperature: {
    celsius: number;
    fahrenheit: number;
  };
  humidity: number;
  conditions: string;
  comfort: 'comfortable' | 'hot' | 'cold' | 'humid';
}

interface LocationData {
  name: string;
  country: string;
  timezone: string;
}

@Component({
  selector: 'app-weather-dashboard',
  template: `
    <div>
      <h3>Weather Dashboard</h3>
      
      <div [ngSwitch]="combinedData.status()">
        <div *ngSwitchCase="'loading'">
          <p>Loading weather data...</p>
        </div>
        
        <div *ngSwitchCase="'error'">
          <p>Error loading data</p>
          <button (click)="refreshData()">Retry</button>
        </div>
        
        <div *ngSwitchCase="'resolved'">
          <div class="weather-card">
            <h4>{{combinedData.value()?.location.name}}, {{combinedData.value()?.location.country}}</h4>
            
            <div class="temperature">
              <span class="temp-c">{{combinedData.value()?.weather.temperature.celsius}}°C</span>
              <span class="temp-f">({{combinedData.value()?.weather.temperature.fahrenheit}}°F)</span>
            </div>
            
            <p>Conditions: {{combinedData.value()?.weather.conditions}}</p>
            <p>Humidity: {{combinedData.value()?.weather.humidity}}%</p>
            <p class="comfort" [class]="combinedData.value()?.weather.comfort">
              Comfort: {{combinedData.value()?.weather.comfort}}
            </p>
            
            <button (click)="refreshData()">Refresh</button>
          </div>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .weather-card {
      border: 1px solid #ccc;
      border-radius: 8px;
      padding: 16px;
      max-width: 400px;
    }
    .temperature {
      font-size: 1.5em;
      margin: 8px 0;
    }
    .temp-f {
      color: #666;
      font-size: 0.8em;
    }
    .comfort.comfortable { color: green; }
    .comfort.hot { color: red; }
    .comfort.cold { color: blue; }
    .comfort.humid { color: orange; }
  `]
})
export class WeatherDashboardComponent {
  private http = inject(HttpClient);
  
  // Raw weather data resource
  private rawWeatherResource = resource<RawWeatherData>(() =>
    this.http.get<RawWeatherData>('/api/weather/current')
  );
  
  // Location data resource
  private locationResource = resource<LocationData>(() =>
    this.http.get<LocationData>('/api/location/current')
  );
  
  // Transform raw weather data
  private processedWeatherResource = mapResource(
    this.rawWeatherResource,
    (raw): ProcessedWeatherData => ({
      temperature: {
        celsius: raw.temperature_celsius,
        fahrenheit: Math.round((raw.temperature_celsius * 9/5) + 32)
      },
      humidity: raw.humidity_percent,
      conditions: raw.conditions,
      comfort: this.calculateComfort(raw.temperature_celsius, raw.humidity_percent)
    })
  );
  
  // Combine both resources
  combinedData = combineResources([
    this.processedWeatherResource,
    this.locationResource
  ]).pipe(
    mapResource(([weather, location]) => ({ weather, location }))
  );
  
  private calculateComfort(temp: number, humidity: number): ProcessedWeatherData['comfort'] {
    if (temp >= 30) return 'hot';
    if (temp <= 10) return 'cold';
    if (humidity >= 70) return 'humid';
    return 'comfortable';
  }
  
  refreshData(): void {
    this.rawWeatherResource.reload();
    this.locationResource.reload();
  }
}

Local Resources and Manual Management

import { Component, signal, resource, localResource } from '@angular/core';

interface Task {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

@Component({
  selector: 'app-task-manager',
  template: `
    <div>
      <h3>Task Manager</h3>
      
      <div class="add-task">
        <input 
          #taskInput
          [(ngModel)]="newTaskTitle"
          placeholder="Enter task title"
          (keyup.enter)="addTask()"
        >
        <button (click)="addTask()" [disabled]="!newTaskTitle.trim()">
          Add Task
        </button>
      </div>
      
      <div class="task-list">
        <div *ngFor="let task of tasksResource.value()" class="task-item">
          <input 
            type="checkbox" 
            [checked]="task.completed"
            (change)="toggleTask(task.id)"
          >
          <span [class.completed]="task.completed">{{task.title}}</span>
          <button (click)="deleteTask(task.id)">Delete</button>
        </div>
      </div>
      
      <div class="stats">
        <p>Total: {{totalTasks()}}</p>
        <p>Completed: {{completedTasks()}}</p>
        <p>Pending: {{pendingTasks()}}</p>
      </div>
      
      <div class="actions">
        <button (click)="clearCompleted()">Clear Completed</button>
        <button (click)="loadSampleTasks()">Load Sample Tasks</button>
      </div>
    </div>
  `,
  styles: [`
    .task-item {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 4px 0;
    }
    .completed {
      text-decoration: line-through;
      color: #666;
    }
    .add-task {
      display: flex;
      gap: 8px;
      margin-bottom: 16px;
    }
    .stats {
      background: #f5f5f5;
      padding: 8px;
      border-radius: 4px;
      margin: 16px 0;
    }
    .actions {
      display: flex;
      gap: 8px;
    }
  `]
})
export class TaskManagerComponent {
  newTaskTitle = '';
  
  // Local resource for managing tasks in memory
  tasksResource = localResource<Task[]>([]);
  
  // Computed properties from resource
  totalTasks = computed(() => this.tasksResource.value()?.length ?? 0);
  completedTasks = computed(() => 
    this.tasksResource.value()?.filter(task => task.completed).length ?? 0
  );
  pendingTasks = computed(() => 
    this.tasksResource.value()?.filter(task => !task.completed).length ?? 0
  );
  
  addTask(): void {
    const title = this.newTaskTitle.trim();
    if (!title) return;
    
    const newTask: Task = {
      id: Date.now(),
      title,
      completed: false,
      createdAt: new Date()
    };
    
    this.tasksResource.update(current => [...(current ?? []), newTask]);
    this.newTaskTitle = '';
  }
  
  toggleTask(taskId: number): void {
    this.tasksResource.update(current => 
      current?.map(task => 
        task.id === taskId 
          ? { ...task, completed: !task.completed }
          : task
      ) ?? []
    );
  }
  
  deleteTask(taskId: number): void {
    this.tasksResource.update(current => 
      current?.filter(task => task.id !== taskId) ?? []
    );
  }
  
  clearCompleted(): void {
    this.tasksResource.update(current => 
      current?.filter(task => !task.completed) ?? []
    );
  }
  
  loadSampleTasks(): void {
    const sampleTasks: Task[] = [
      { id: 1, title: 'Learn Angular Resources', completed: false, createdAt: new Date() },
      { id: 2, title: 'Build a todo app', completed: true, createdAt: new Date() },
      { id: 3, title: 'Write documentation', completed: false, createdAt: new Date() }
    ];
    
    this.tasksResource.set(sampleTasks);
  }
}

Server-Side Rendering Support

import { Component, resource, preloadResource } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { isPlatformServer } from '@angular/common';
import { PLATFORM_ID, inject } from '@angular/core';

interface Article {
  id: number;
  title: string;
  content: string;
  author: string;
  publishedAt: string;
}

@Component({
  selector: 'app-article',
  template: `
    <article>
      <div [ngSwitch]="articleResource.status()">
        <div *ngSwitchCase="'loading'">
          <div class="skeleton">
            <div class="skeleton-title"></div>
            <div class="skeleton-content"></div>
          </div>
        </div>
        
        <div *ngSwitchCase="'resolved'">
          <header>
            <h1>{{articleResource.value()?.title}}</h1>
            <p class="meta">
              By {{articleResource.value()?.author}} | 
              {{articleResource.value()?.publishedAt | date}}
            </p>
          </header>
          
          <div class="content" [innerHTML]="articleResource.value()?.content">
          </div>
        </div>
        
        <div *ngSwitchCase="'error'">
          <div class="error">
            <h2>Article Not Found</h2>
            <p>The requested article could not be loaded.</p>
            <button (click)="articleResource.reload()">Try Again</button>
          </div>
        </div>
      </div>
    </article>
  `,
  styles: [`
    .skeleton {
      animation: pulse 1.5s ease-in-out infinite;
    }
    .skeleton-title {
      height: 32px;
      background: #e2e8f0;
      border-radius: 4px;
      margin-bottom: 16px;
    }
    .skeleton-content {
      height: 200px;
      background: #e2e8f0;
      border-radius: 4px;
    }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }
    .meta {
      color: #666;
      font-size: 0.9em;
    }
    .content {
      line-height: 1.6;
      margin-top: 24px;
    }
    .error {
      text-align: center;
      padding: 48px 16px;
    }
  `]
})
export class ArticleComponent {
  private http = inject(HttpClient);
  private platformId = inject(PLATFORM_ID);
  
  // Get article ID from route or input
  articleId = signal(1);
  
  // Resource with SSR optimization
  articleResource = resource<Article, number>({
    request: () => this.articleId(),
    loader: async (id) => {
      // Preload on server for faster initial render
      if (isPlatformServer(this.platformId)) {
        const article = await this.http.get<Article>(`/api/articles/${id}`).toPromise();
        if (article) {
          // Cache for client hydration
          return article;
        }
      }
      
      return this.http.get<Article>(`/api/articles/${id}`);
    }
  });
  
  ngOnInit(): void {
    // Preload resource data during SSR
    if (isPlatformServer(this.platformId)) {
      preloadResource(this.articleResource);
    }
  }
}