Angular bindings for OpenUI Lang. Define model-renderable Angular components, generate prompts from those definitions, and render streamed OpenUI Lang in an Angular app.
Links: OpenUI Lang docs | GitHub repo
npm install @openuidev/angular-lang
# or
pnpm add @openuidev/angular-langPeer dependencies: @angular/core, @angular/common, rxjs, zod
@openuidev/angular-lang brings the OpenUI Lang runtime to Angular:
- Define Angular components that a model is allowed to call, with Zod schemas for props.
- Generate prompts from the component library.
- Render streamed output with
Renderer/<openui-renderer>as OpenUI Lang arrives.
import { Component, Input } from "@angular/core";
import { defineComponent } from "@openuidev/angular-lang";
import { z } from "zod/v4";
@Component({
selector: "demo-greeting",
standalone: true,
template: `<div>Hello, {{ props?.name }}!</div>`,
})
export class DemoGreetingComponent {
@Input() props: { name: string; mood?: "happy" | "excited" } | null = null;
@Input() renderNode: ((value: unknown) => unknown) | null = null;
@Input() statementId: string | undefined = undefined;
}
export const Greeting = defineComponent({
name: "Greeting",
description: "Displays a greeting message",
props: z.object({
name: z.string().describe("The person's name"),
mood: z.enum(["happy", "excited"]).optional().describe("Tone of the greeting"),
}),
component: DemoGreetingComponent,
});import { createLibrary } from "@openuidev/angular-lang";
export const library = createLibrary({
components: [Greeting],
root: "Greeting",
});const systemPrompt = library.prompt({
preamble: "You are a helpful assistant.",
additionalRules: ["Always greet the user by name."],
examples: ['root = Greeting("Alice", "happy")'],
});import { Component } from "@angular/core";
import { Renderer } from "@openuidev/angular-lang";
@Component({
selector: "assistant-message",
standalone: true,
imports: [Renderer],
template: `
<openui-renderer
[response]="response"
[library]="library"
[isStreaming]="isStreaming"
(action)="handleAction($event)"
/>
`,
})
export class AssistantMessageComponent {
response: string | null = null;
isStreaming = false;
library = library;
handleAction(event: unknown) {
console.log("Action:", event);
}
}| Export | Description |
|---|---|
defineComponent(config) |
Define a single component with a name, Zod props schema, description, and Angular renderer |
createLibrary(definition) |
Create a library from an array of defined components |
| Export | Description |
|---|---|
Renderer |
Standalone Angular component class for OpenUI Lang rendering |
OpenUiRendererComponent |
Named Angular component export for the same renderer |
<openui-renderer> |
Renderer selector used in templates |
RendererProps:
| Prop | Type | Description |
|---|---|---|
response |
string | null |
Raw OpenUI Lang text from the model |
library |
Library |
Component library from createLibrary() |
isStreaming |
boolean |
Whether the model is still streaming |
onAction / (action) |
(event: ActionEvent) => void |
Callback or output when a component triggers an action |
onStateUpdate / (stateUpdate) |
(state: Record<string, unknown>) => void |
Callback or output when form field values change |
initialState |
Record<string, unknown> |
Initial form state for hydration |
onParseResult / (parseResult) |
(result: ParseResult | null) => void |
Callback or output when the parse result changes |
toolProvider |
Record<string, Function> | McpClientLike | null |
Tool provider for executing Query() and Mutation() calls |
queryLoader |
Type<unknown> | null |
Custom Angular loading component shown during query loading |
onError / (error) |
(errors: OpenUIError[]) => void |
Callback or output for structured parser, query, and render errors |
ParseResult.meta.errors contains structured OpenUIError objects. Each error has a code for consumer-side filtering:
| Code | Meaning |
|---|---|
missing-required |
Required prop absent with no default |
null-required |
Required prop explicitly null with no default |
unknown-component |
Component name not found in the library schema |
excess-args |
More positional args passed than the schema defines |
tool-not-found |
A Query() or Mutation() tool was not registered |
tool-error |
A registered query tool threw an error |
mcp-error |
An MCP tool call returned an MCP error envelope |
render-error |
An Angular component threw during render and the last good subtree was preserved |
Errors stay structured so host apps can log them, surface them, or feed them back into an automated correction loop.
| Export | Description |
|---|---|
createParser(library) |
Create a one-shot parser for complete OpenUI Lang text |
createStreamingParser(library) |
Create an incremental parser for streaming input |
generatePrompt(spec) |
Generate OpenUI prompt text from a prompt spec |
generateSystemPrompt(spec) |
Generate a complete system prompt |
mergeStatements(...) |
Merge statement fragments into stable OpenUI Lang output |
parse(source) |
Parse a full OpenUI Lang source string |
Use these inside Angular component renderers to interact with the OpenUI runtime.
Call injection helpers in a constructor or field initializer. In lifecycle hooks
such as ngOnChanges, pass an already-injected context to
setDefaultValue(options, this.openUi); hooks are not injection contexts.
| Helper | Description |
|---|---|
injectOpenUiContext() |
Access the full OpenUI runtime context |
injectRenderNode() |
Get the recursive child renderer |
injectTriggerAction() |
Trigger an action event |
injectIsStreaming() |
Return a Signal<boolean> that tracks whether the model is streaming |
injectIsQueryLoading() |
Return a Signal<boolean> that tracks whether any query is loading |
injectGetFieldValue() |
Read a form field's current value |
injectSetFieldValue() |
Set a form field's value |
injectFormName() |
Get the current form name |
injectStore() |
Access the underlying store |
injectEvaluationContext() |
Access the runtime evaluation context |
setDefaultValue(options, context?) |
Persist a default field value once streaming finishes |
Capture the streaming/loading signals once during component construction, then call them in templates or event handlers:
readonly isStreaming = injectIsStreaming();
readonly isQueryLoading = injectIsQueryLoading();
// Template: <button [disabled]="isStreaming() || isQueryLoading()">Submit</button>The full context's isStreaming and isQueryLoading properties remain boolean
getters backed by those reactive values. Reading them in a template also tracks
updates. getFieldValue(formName, name) tracks store changes when called from a
template or computed(), including components whose OpenUI props never change.
Read the field inside that reactive consumer instead of saving its initial value.
| Export | Description |
|---|---|
injectFormValidation() |
Access form validation state |
createFormValidation() |
Create a validation context |
provideFormValidation() |
Provide validation state to Angular subtrees |
validate(value, rules) |
Run validation rules against a value |
builtInValidators |
Built-in validators such as required, email, min, and max |
import type {
Library,
LibraryDefinition,
DefinedComponent,
ComponentRenderer,
ComponentRenderProps,
ComponentGroup,
PromptOptions,
RendererProps,
ActionEvent,
ActionPlan,
ElementNode,
ParseResult,
OpenUIError,
LibraryJSONSchema,
McpClientLike,
ToolProvider,
ValidationErrorCode,
} from "@openuidev/angular-lang";OpenUI Lang connects to your backend through tools. You can register a toolProvider to handle data fetching (Query()) and updates (Mutation()) natively in Angular:
toolProvider = {
async get_server_health() {
const res = await fetch("/api/health");
return res.json();
},
async create_ticket(args: Record<string, unknown>) {
const res = await fetch("/api/tickets", {
method: "POST",
body: JSON.stringify(args),
headers: { "Content-Type": "application/json" },
});
return res.json();
},
};The renderer accepts either:
- a plain async function map
- an MCP-like client with
callTool({ name, arguments })
Angular components registered with defineComponent() should accept these inputs:
propsrenderNodestatementId
Nested rendering, state access, query loading state, and action dispatch all flow through the injected OpenUI context.
From the workspace root:
pnpm --filter @openuidev/angular-lang test
pnpm --filter @openuidev/angular-lang typecheck
pnpm --filter @openuidev/angular-lang build
pnpm --filter @openuidev/angular-lang lint:check
pnpm --filter @openuidev/angular-lang format:check
pnpm --filter @openuidev/angular-lang packThis package declares its development Node.js runtime through
devEngines.runtime. With the repository's pinned pnpm version, installation
provisions Node.js 24.15.0 and package scripts use it without changing the Node.js
version used by other workspace packages.
ng-packagr builds the library into this package's dist/ folder. The package
manifest declares the runtime and type entry points directly, and pnpm pack
resolves workspace and catalog dependencies when creating the archive. It does
not publish to npm.
Suggested manual smoke-test flow:
- Register a tiny standalone library with two or three demo components.
- Render a static response.
- Render nested child references through
renderNode. - Hydrate
initialStateand confirm field reads. - Trigger
setFieldValue()and verify(stateUpdate). - Execute a
Query()through a mocktoolProvider. - Execute a
Mutation()through arunaction step. - Confirm custom
queryLoaderbehavior during an in-flight query. - Confirm
(error)receives structured parser, tool, and render errors.
Libraries can also produce a JSON Schema representation of their components:
const schema = library.toJSONSchema();
// schema.$defs["Greeting"] -> { properties: {...}, required: [...] }