spoosh-angular

Use this skill when the user asks about "Spoosh", "injectRead", "injectWrite", "injectPages", "injectQueue", "Spoosh Angular", "Spoosh injects", "Spoosh plugins", "cache plugin", "retry plugin", "polling plugin", "optimistic updates", "invalidation", "data fetching component", "mutation component", "infinite scroll", "Spoosh patterns", or needs to build Angular components with type-safe API calls. Provides comprehensive API knowledge and component patterns for @spoosh/angular.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "spoosh-angular" with this command: npx skills add spooshdev/skills/spooshdev-skills-spoosh-angular

Spoosh Angular

Spoosh is a type-safe API toolkit with a composable plugin architecture for TypeScript. This skill covers the Angular integration using signals including inject functions API, plugins, and component patterns.

Setup

pnpm add @spoosh/core @spoosh/angular
import { Spoosh } from "@spoosh/core";
import { create } from "@spoosh/angular";

type ApiSchema = {
  "users": {
    GET: { data: User[] };
    POST: { data: User; body: CreateUserBody };
  };
  "users/:id": {
    GET: { data: User };
    DELETE: { data: void };
  };
};

const spoosh = new Spoosh<ApiSchema, Error>("/api")
  .use([cachePlugin(), retryPlugin()]);

export const { injectRead, injectWrite, injectPages, injectQueue } = create(spoosh);

Inject Functions API

injectRead

Fetch data with automatic caching and state management. Returns signals.

const users = injectRead(
  (api) => api("users").GET(),
  { staleTime: 30000, enabled: true }
);

Returns (all signals except methods): data, loading, fetching, error, trigger(), abort(), meta

Options: enabled (supports Signal or function), tags, staleTime, retry, pollingInterval, refetch, debounce, transform, initialData

injectWrite

Perform mutations (POST, PUT, DELETE). Returns signals.

const createUser = injectWrite((api) => api("users").POST());

await createUser.trigger({
  body: { name, email },
  invalidate: "all"
});

Returns (all signals except methods): trigger(), loading, error, data, meta, input, abort()

Trigger options: body, params, query, headers, invalidate, clearCache, optimistic

injectPages

Bidirectional pagination with infinite scroll. Returns signals.

const posts = injectPages(
  (api) => api("posts").GET({ query: { page: 1 } }),
  {
    canFetchNext: ({ lastPage }) => lastPage?.data?.hasMore ?? false,
    nextPageRequest: ({ lastPage, request }) => ({
      query: { ...request.query, page: (lastPage?.data?.page ?? 0) + 1 }
    }),
    merger: (pages) => pages.flatMap(p => p.data?.items ?? [])
  }
);

Returns (all signals except methods): data, pages, loading, fetchingNext, canFetchNext, fetchNext(), fetchPrev(), trigger()

injectQueue

Queue management for batch operations with concurrency control.

const uploadQueue = injectQueue(
  (api) => api("files").POST(),
  { concurrency: 3 }
);

files.forEach(file => uploadQueue.trigger({ body: form({ file }) }));

Returns (signals for tasks and stats): tasks, stats, trigger(), abort(), retry(), remove(), removeSettled(), clear(), setConcurrency()

Stats: pending, loading, settled, success, failed, total, percentage

Plugins

PluginPurposeKey Options
cachePluginResponse cachingstaleTime
retryPluginAutomatic retriesretry: { retries, delay }
pollingPluginAuto-refreshpollingInterval
invalidationPluginCache invalidationinvalidate
optimisticPluginInstant UI updatesoptimistic
debouncePluginDebounce requestsdebounce
refetchPluginRefetch on focusrefetch: { onFocus, onReconnect }

Cache Invalidation

// After mutation
await createUser.trigger({ body: data, invalidate: "all" });    // Invalidate hierarchy
await createUser.trigger({ body: data, invalidate: "self" });   // Exact path only
await createUser.trigger({ body: data, invalidate: ["users"] }); // Specific tags

Component Patterns

Data Fetching

@Component({
  selector: "app-user-list",
  template: `
    @if (users.loading()) {
      <app-user-list-skeleton />
    } @else if (users.error()) {
      <app-error-message [error]="users.error()!" (retry)="users.trigger()" />
    } @else if (!users.data()?.length) {
      <app-empty-state message="No users found" />
    } @else {
      <ul>
        @for (user of users.data(); track user.id) {
          <app-user-card [user]="user" />
        }
      </ul>
    }
  `,
})
export class UserListComponent {
  users = injectRead((api) => api("users").GET(), { staleTime: 30000 });
}

Mutation Form

@Component({
  selector: "app-create-user-form",
  template: `
    <form (ngSubmit)="handleSubmit()">
      <input [(ngModel)]="name" name="name" [disabled]="createUser.loading()" />
      @if (createUser.error()) {
        <p class="error">{{ createUser.error()!.message }}</p>
      }
      <button [disabled]="createUser.loading()">
        {{ createUser.loading() ? "Creating..." : "Create" }}
      </button>
    </form>
  `,
})
export class CreateUserFormComponent {
  name = "";
  createUser = injectWrite((api) => api("users").POST());

  async handleSubmit() {
    const result = await this.createUser.trigger({
      body: { name: this.name },
      invalidate: "all",
    });
    if (result.data) this.name = "";
  }
}

Infinite Scroll

@Component({
  selector: "app-infinite-post-list",
  template: `
    @if (posts.loading()) {
      <app-post-list-skeleton />
    } @else {
      <div>
        @for (post of posts.data(); track post.id) {
          <app-post-card [post]="post" />
        }
        <div #loadTrigger>
          @if (posts.fetchingNext()) {
            <app-loading-spinner />
          }
        </div>
      </div>
    }
  `,
})
export class InfinitePostListComponent {
  loadTrigger = viewChild<ElementRef>("loadTrigger");

  posts = injectPages(
    (api) => api("posts").GET({ query: { page: 1, limit: 20 } }),
    {
      canFetchNext: ({ lastPage }) => lastPage?.data?.hasMore ?? false,
      nextPageRequest: ({ lastPage, request }) => ({
        query: { ...request.query, page: (lastPage?.data?.page ?? 0) + 1 },
      }),
      merger: (pages) => pages.flatMap((p) => p.data?.items ?? []),
    }
  );

  private observer?: IntersectionObserver;

  constructor() {
    effect(() => {
      const element = this.loadTrigger()?.nativeElement;
      if (element) {
        this.observer?.disconnect();
        this.observer = new IntersectionObserver(
          (entries) => {
            if (entries[0].isIntersecting && this.posts.canFetchNext() && !this.posts.fetchingNext()) {
              this.posts.fetchNext();
            }
          },
          { threshold: 0.1 }
        );
        this.observer.observe(element);
      }
    });
  }
}

Optimistic Updates

@Component({
  selector: "app-toggle-like-button",
  template: `
    <button (click)="handleToggle()">{{ liked() ? "Unlike" : "Like" }} ({{ likeCount() }})</button>
  `,
})
export class ToggleLikeButtonComponent {
  postId = input.required<string>();
  liked = input.required<boolean>();
  likeCount = input.required<number>();

  toggleLike = injectWrite((api) => api("posts/:id/like").POST());

  handleToggle() {
    this.toggleLike.trigger({
      params: { id: this.postId() },
      optimistic: (cache) => cache(`posts/${this.postId()}`)
        .set((current) => ({
          ...current,
          liked: !this.liked(),
          likeCount: this.liked() ? this.likeCount() - 1 : this.likeCount() + 1,
        }))
    });
  }
}

Search with Debounce

@Component({
  selector: "app-search-users",
  template: `
    <div>
      <input [(ngModel)]="query" (ngModelChange)="searchQuery.set($event)" placeholder="Search..." />
      @if (searchResults.fetching()) {
        <app-loading-indicator />
      }
      @for (user of searchResults.data(); track user.id) {
        <li>{{ user.name }}</li>
      }
    </div>
  `,
})
export class SearchUsersComponent {
  query = "";
  searchQuery = signal("");

  searchResults = injectRead(
    (api) => api("users/search").GET({ query: { q: this.searchQuery() } }),
    { enabled: () => this.searchQuery().length >= 2, debounce: 300 }
  );
}

Polling

@Component({
  selector: "app-job-status",
  template: `<p>Status: {{ job.data()?.status }}</p>`,
})
export class JobStatusComponent {
  jobId = input.required<string>();

  job = injectRead(
    (api) => api("jobs/:id").GET({ params: { id: this.jobId() } }),
    {
      pollingInterval: ({ data }) => {
        if (data?.status === "completed" || data?.status === "failed") return false;
        return 2000;
      }
    }
  );
}

Server Type Inference

Hono

import { Spoosh, StripPrefix } from "@spoosh/core";
import type { HonoToSpoosh } from "@spoosh/hono";

// Server: app.basePath("/api")
type FullSchema = HonoToSpoosh<typeof app>;
type ApiSchema = StripPrefix<FullSchema, "api">; // Avoid double /api/api

const spoosh = new Spoosh<ApiSchema, Error>("/api");

Elysia

import { Spoosh, StripPrefix } from "@spoosh/core";
import type { ElysiaToSpoosh } from "@spoosh/elysia";

// Server: new Elysia({ prefix: "/api" })
type FullSchema = ElysiaToSpoosh<typeof app>;
type ApiSchema = StripPrefix<FullSchema, "api">; // Avoid double /api/api

const spoosh = new Spoosh<ApiSchema, Error>("/api");

Use StripPrefix when your baseUrl includes the same prefix as the server's basePath to prevent double prefixing (e.g., /api/api/users).

OpenAPI

# Export TypeScript → OpenAPI
npx spoosh-openapi export --schema ./schema.ts --output openapi.json

# Import OpenAPI → TypeScript
npx spoosh-openapi import openapi.json --output ./schema.ts

References

For detailed API documentation:

  • references/signals-api.md - Complete inject function signatures
  • references/plugins-api.md - All plugin configurations
  • references/advanced-patterns.md - Complex patterns and edge cases

If more detail needed, fetch https://spoosh.dev/docs/angular/llms (or /llms-full for complete docs).

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

spoosh-react

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

Self Updater

⭐ OPEN SOURCE! GitHub: github.com/GhostDragon124/openclaw-self-updater ⭐ ONLY skill with Cron-aware + Idle detection! Auto-updates OpenClaw core & skills, an...

Registry SourceRecently Updated
1171Profile unavailable
Coding

ClawHub CLI Assistant

Use the ClawHub CLI to publish, inspect, version, update, sync, and troubleshoot OpenClaw skills from the terminal.

Registry SourceRecently Updated
1.9K2Profile unavailable
Coding

SkillTree Learning Progress Tracker

Track learning across topics like an RPG skill tree. Prerequisites, milestones, suggested next steps. Gamified learning path.

Registry SourceRecently Updated
900Profile unavailable