Angular signal-based bindings for TanStack AI, providing convenient Angular bindings for the headless client.
Injection context requirement: Every inject* function in this package calls Angular's inject() internally. They must be called within an Angular injection context — a component or directive class field initializer, the constructor, or inside runInInjectionContext. Calling them outside an injection context will throw a runtime error.
npm install @tanstack/ai-angularnpm install @tanstack/ai-angularMain injectable for managing chat state in Angular with full type safety.
import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
template: `...`,
})
export class ChatComponent {
// injectChat is called in a field initializer — valid injection context.
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
template: `...`,
})
export class ChatComponent {
// injectChat is called in a field initializer — valid injection context.
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}Extends ChatClientOptions from @tanstack/ai-client (minus internal state callbacks):
Reactive options (body, forwardedProps, context, live) accept a ReactiveOption<T>, which is one of:
type ReactiveOption<T> = T | Signal<T> | (() => T);type ReactiveOption<T> = T | Signal<T> | (() => T);A plain value becomes a constant; a Signal is read directly; a zero-arg getter is wrapped in computed so any signals read inside it are tracked.
Note: Client tools are automatically executed — no onToolCall callback needed!
interface InjectChatResult {
messages: Signal<UIMessage[]>;
sendMessage: (content: string | MultimodalContent) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
clear: () => void;
setMessages: (messages: UIMessage[]) => void;
isLoading: Signal<boolean>;
error: Signal<Error | undefined>;
status: Signal<ChatClientState>;
isSubscribed: Signal<boolean>;
connectionStatus: Signal<ConnectionStatus>;
sessionGenerating: Signal<boolean>;
// Only present when outputSchema is supplied:
partial: Signal<DeepPartial<InferSchemaType<TSchema>>>;
final: Signal<InferSchemaType<TSchema> | null>;
}interface InjectChatResult {
messages: Signal<UIMessage[]>;
sendMessage: (content: string | MultimodalContent) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
clear: () => void;
setMessages: (messages: UIMessage[]) => void;
isLoading: Signal<boolean>;
error: Signal<Error | undefined>;
status: Signal<ChatClientState>;
isSubscribed: Signal<boolean>;
connectionStatus: Signal<ConnectionStatus>;
sessionGenerating: Signal<boolean>;
// Only present when outputSchema is supplied:
partial: Signal<DeepPartial<InferSchemaType<TSchema>>>;
final: Signal<InferSchemaType<TSchema> | null>;
}Note: All reactive state (messages, isLoading, error, status, isSubscribed, connectionStatus, sessionGenerating) is exposed as read-only Angular Signals. Read them by calling them as functions (e.g., chat.messages(), chat.isLoading()). Cleanup is automatic via DestroyRef.onDestroy.
Re-exported from @tanstack/ai-client for convenience:
import {
fetchServerSentEvents,
fetchHttpStream,
xhrServerSentEvents,
xhrHttpStream,
stream,
rpcStream,
type ConnectionAdapter,
} from "@tanstack/ai-angular";import {
fetchServerSentEvents,
fetchHttpStream,
xhrServerSentEvents,
xhrHttpStream,
stream,
rpcStream,
type ConnectionAdapter,
} from "@tanstack/ai-angular";import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
imports: [CommonModule],
template: `
<ul>
@for (message of chat.messages(); track message.id) {
<li>
<strong>{{ message.role }}:</strong>
@for (part of message.parts; track $index) {
@if (part.type === 'thinking') {
<em>Thinking: {{ part.content }}</em>
} @else if (part.type === 'text') {
<span>{{ part.content }}</span>
}
}
</li>
}
</ul>
<input #input placeholder="Type a message..." />
<button
(click)="chat.sendMessage(input.value); input.value = ''"
[disabled]="chat.isLoading()"
>
Send
</button>
@if (chat.isLoading()) {
<p>Thinking...</p>
}
`,
})
export class ChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
imports: [CommonModule],
template: `
<ul>
@for (message of chat.messages(); track message.id) {
<li>
<strong>{{ message.role }}:</strong>
@for (part of message.parts; track $index) {
@if (part.type === 'thinking') {
<em>Thinking: {{ part.content }}</em>
} @else if (part.type === 'text') {
<span>{{ part.content }}</span>
}
}
</li>
}
</ul>
<input #input placeholder="Type a message..." />
<button
(click)="chat.sendMessage(input.value); input.value = ''"
[disabled]="chat.isLoading()"
>
Send
</button>
@if (chat.isLoading()) {
<p>Thinking...</p>
}
`,
})
export class ChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-approval-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (
part.type === 'tool-call' &&
part.state === 'approval-requested' &&
part.approval
) {
<div>
<p>Approve: {{ part.name }}</p>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: true })">
Approve
</button>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: false })">
Deny
</button>
</div>
}
}
}
`,
})
export class ApprovalChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-approval-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (
part.type === 'tool-call' &&
part.state === 'approval-requested' &&
part.approval
) {
<div>
<p>Approve: {{ part.name }}</p>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: true })">
Approve
</button>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: false })">
Deny
</button>
</div>
}
}
}
`,
})
export class ApprovalChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import {
clientTools,
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
import { updateUIDef, saveToStorageDef } from "./tool-definitions";
@Component({
selector: "app-typed-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (part.type === 'tool-call' && part.name === 'updateUI') {
<div>Tool executed: {{ part.name }}</div>
}
}
}
`,
})
export class TypedChatComponent {
// Create client implementations
private updateUI = updateUIDef.client((input) => {
// input is fully typed!
return { success: true };
});
private saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
private tools = clientTools(this.updateUI, this.saveToStorage);
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
tools: this.tools, // Automatic execution, full type safety
});
}import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import {
clientTools,
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
import { updateUIDef, saveToStorageDef } from "./tool-definitions";
@Component({
selector: "app-typed-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (part.type === 'tool-call' && part.name === 'updateUI') {
<div>Tool executed: {{ part.name }}</div>
}
}
}
`,
})
export class TypedChatComponent {
// Create client implementations
private updateUI = updateUIDef.client((input) => {
// input is fully typed!
return { success: true };
});
private saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
private tools = clientTools(this.updateUI, this.saveToStorage);
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
tools: this.tools, // Automatic execution, full type safety
});
}import { Component, signal } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-reactive-chat",
standalone: true,
template: `
<button (click)="toggleLanguage()">Toggle Language</button>
@for (message of chat.messages(); track message.id) {
<p>{{ message.role }}: {{ message.parts[0]?.content }}</p>
}
`,
})
export class ReactiveChatComponent {
language = signal("en");
// forwardedProps is reactive — the signal is read on every request
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
forwardedProps: () => ({ language: this.language() }),
});
toggleLanguage() {
this.language.set(this.language() === "en" ? "fr" : "en");
}
}import { Component, signal } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-reactive-chat",
standalone: true,
template: `
<button (click)="toggleLanguage()">Toggle Language</button>
@for (message of chat.messages(); track message.id) {
<p>{{ message.role }}: {{ message.parts[0]?.content }}</p>
}
`,
})
export class ReactiveChatComponent {
language = signal("en");
// forwardedProps is reactive — the signal is read on every request
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
forwardedProps: () => ({ language: this.language() }),
});
toggleLanguage() {
this.language.set(this.language() === "en" ? "fr" : "en");
}
}import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import { z } from "zod";
const recipeSchema = z.object({
title: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
});
@Component({
selector: "app-recipe-chat",
standalone: true,
template: `
<button (click)="chat.sendMessage('Give me a pasta recipe')">Ask</button>
@if (chat.partial().title) {
<h2>{{ chat.partial().title }}</h2>
}
@if (chat.final()) {
<ul>
@for (step of chat.final()!.steps; track $index) {
<li>{{ step }}</li>
}
</ul>
}
`,
})
export class RecipeChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
outputSchema: recipeSchema,
});
}import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import { z } from "zod";
const recipeSchema = z.object({
title: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
});
@Component({
selector: "app-recipe-chat",
standalone: true,
template: `
<button (click)="chat.sendMessage('Give me a pasta recipe')">Ask</button>
@if (chat.partial().title) {
<h2>{{ chat.partial().title }}</h2>
}
@if (chat.final()) {
<ul>
@for (step of chat.final()!.steps; track $index) {
<li>{{ step }}</li>
}
</ul>
}
`,
})
export class RecipeChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
outputSchema: recipeSchema,
});
}Angular injectables for one-shot generation tasks (images, audio, speech, transcription, summarization, video). All share the same pattern: provide a connection or fetcher, call generate(), and read reactive signals.
Base injectable for custom generation types. All specialized injectables below are built on this.
import { Component } from "@angular/core";
import { injectGeneration } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({ selector: "app-custom", standalone: true, template: `...` })
export class CustomGenerationComponent {
gen = injectGeneration({
connection: fetchServerSentEvents("/api/generate/custom"),
});
// Call gen.generate(input), read gen.result(), gen.isLoading(), etc.
}import { Component } from "@angular/core";
import { injectGeneration } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({ selector: "app-custom", standalone: true, template: `...` })
export class CustomGenerationComponent {
gen = injectGeneration({
connection: fetchServerSentEvents("/api/generate/custom"),
});
// Call gen.generate(input), read gen.result(), gen.isLoading(), etc.
}Options: connection?, fetcher?, id?, body? (reactive), devtools?, onResult?, onError?, onProgress?, onChunk?
Returns: generate, result, isLoading, error, status, stop, reset — all reactive state is a read-only Signal<T>.
Image generation injectable. generate() accepts ImageGenerateInput, result is ImageGenerationResult.
import { Component } from "@angular/core";
import { injectGenerateImage } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-image",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A mountain at sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<img [src]="gen.result()!.images[0]!.url" alt="Generated image" />
}
`,
})
export class ImageComponent {
gen = injectGenerateImage({
connection: fetchServerSentEvents("/api/generate/image"),
});
}import { Component } from "@angular/core";
import { injectGenerateImage } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-image",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A mountain at sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<img [src]="gen.result()!.images[0]!.url" alt="Generated image" />
}
`,
})
export class ImageComponent {
gen = injectGenerateImage({
connection: fetchServerSentEvents("/api/generate/image"),
});
}Audio generation injectable (music, sound effects). generate() accepts AudioGenerateInput, result is AudioGenerationResult.
import { Component } from "@angular/core";
import { injectGenerateAudio } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-audio",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'An upbeat electronic track', duration: 10 })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<audio [src]="gen.result()!.audio.url" controls></audio>
}
`,
})
export class AudioComponent {
gen = injectGenerateAudio({
connection: fetchServerSentEvents("/api/generate/audio"),
});
}import { Component } from "@angular/core";
import { injectGenerateAudio } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-audio",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'An upbeat electronic track', duration: 10 })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<audio [src]="gen.result()!.audio.url" controls></audio>
}
`,
})
export class AudioComponent {
gen = injectGenerateAudio({
connection: fetchServerSentEvents("/api/generate/audio"),
});
}Text-to-speech injectable. generate() accepts SpeechGenerateInput, result is TTSResult.
Audio transcription injectable. generate() accepts TranscriptionGenerateInput, result is TranscriptionResult.
Text summarization injectable. generate() accepts SummarizeGenerateInput, result is SummarizationResult.
Video generation injectable with job polling. Returns additional jobId and videoStatus signals. Accepts extra onJobCreated? and onStatusUpdate? callbacks.
import { Component } from "@angular/core";
import { injectGenerateVideo } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-video",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A time-lapse of a sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.videoStatus()) {
<p>Status: {{ gen.videoStatus()!.status }}</p>
}
@if (gen.result()) {
<video [src]="gen.result()!.url" controls></video>
}
`,
})
export class VideoComponent {
gen = injectGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
onJobCreated: (jobId) => console.log("Job created:", jobId),
});
}import { Component } from "@angular/core";
import { injectGenerateVideo } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-video",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A time-lapse of a sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.videoStatus()) {
<p>Status: {{ gen.videoStatus()!.status }}</p>
}
@if (gen.result()) {
<video [src]="gen.result()!.url" controls></video>
}
`,
})
export class VideoComponent {
gen = injectGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
onJobCreated: (jobId) => console.log("Job created:", jobId),
});
}Additional returns (video only):
All generation injectables automatically clean up via DestroyRef.onDestroy.
Angular's DI system requires that inject() is called during component construction. Every inject* function in this package calls inject() internally. Valid call sites:
// Field initializer (recommended)
export class MyComponent {
chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
// Constructor
export class MyComponent {
chat: ReturnType<typeof injectChat>;
constructor() {
this.chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
}
// Inside runInInjectionContext
const chat = runInInjectionContext(injector, () =>
injectChat({ connection: fetchServerSentEvents("/api/chat") }),
);// Field initializer (recommended)
export class MyComponent {
chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
// Constructor
export class MyComponent {
chat: ReturnType<typeof injectChat>;
constructor() {
this.chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
}
// Inside runInInjectionContext
const chat = runInInjectionContext(injector, () =>
injectChat({ connection: fetchServerSentEvents("/api/chat") }),
);Helper to create typed chat options (re-exported from @tanstack/ai-client).
import {
clientTools,
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(tool1, tool2);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type Messages = InferChatMessages<typeof chatOptions>;import {
clientTools,
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(tool1, tool2);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type Messages = InferChatMessages<typeof chatOptions>;Re-exported from @tanstack/ai-angular (sourced from @tanstack/ai-client):
Tool authoring types — import directly from @tanstack/ai (not re-exported by @tanstack/ai-angular):