UI Component System Overview
Overview
UDIOT ships a set of server-renderable UI components (Button, Card, Input, Modal, DragDropManager, SortableList) alongside a UIComponentRegistry for component registration and discovery. Components extend BaseComponent (from src/components.ts) and are decorated with @Component({ selector }). They use @InjectProperty() to receive services such as ThemeService and ValidationService from the DI container, and optionally accept an AIStyler instance for AI-generated CSS.
Key Concepts
BaseComponent<TProps>— base class for all UI components. Provides apropsaccessor, anonInit()lifecycle hook, and an abstractrender(): stringmethod that returns an HTML string.@Component({ selector })— class decorator that registers the component selector (e.g.udiot-button) so it can be discovered by the renderer.@InjectProperty()— property decorator that wires the DI container into a component field at instantiation time. Used forThemeService,ValidationService, and any other injectable service.ThemeService— service (fromsrc/services/ui/) that exposesgetTheme(): 'light' | 'dark'andsubscribe(callback)for reactive theme changes.AIStyler— optional styling bridge (fromsrc/styling.ts) that generates component CSS by calling an AI model. When omitted, components use built-in theme-aware fallback styles.UIComponentRegistry— singletoncomponentRegistrythat maps component names toComponentConfigrecords, including an optionalAIClientandAIStylerfor AI-driven component generation.createXxxfactories — each component module exports a factory function (e.g.createButton(props)) as a convenience alternative to callingnew Button(props).
DI integration and limitations
Public components use internal theme/validation services. Those service classes
are not public /services/ui or /services/api imports. Do not assume that
constructing a component wires them automatically or that a validationRules
prop enforces server-side validation. Use application-owned validation and
verify component behavior with your chosen lifecycle/DI integration.
The root DI component decorator is named ComponentDecorator; Component
is an interface. The UI helpers described here, including Modal and the registry,
are not exported by @tagna/udiot/browser. Do not import the root entry into
client bundles or assume server-rendered markup automatically becomes interactive.
Use application-owned client UI behavior. Components and generated styling remain experimental.
AI Styling
Pass an AIStyler instance as the aiStyler prop to have the component's CSS generated at render time by an AI model:
import { createButton } from '@tagna/udiot';
import { AIStyler } from '@tagna/udiot';
const styler = new AIStyler(aiClient);
const button = createButton({
children: 'Save',
variant: 'primary',
aiStyler: styler,
});
console.log(button.render()); // <style>/* AI-generated CSS */</style><button ...>Save</button>
When aiStyler is omitted, the component generates its own styles using CSS variables derived from ThemeService.getTheme().
UIComponentRegistry
interface ComponentConfig {
name: string;
description: string;
aiClient?: AIClient;
styler?: AIStyler;
props?: Record<string, any>;
}
class UIComponentRegistry {
register(config: ComponentConfig): void;
get(name: string): ComponentConfig | undefined;
list(): string[];
generateComponent(name: string, description: string, aiClient: AIClient): Promise<string>;
}
// Public singleton; register application configurations explicitly
const componentRegistry: UIComponentRegistry;
The root export exposes componentRegistry but does not import the internal UI auto-registration barrel. Register application component configurations explicitly. Modal and DragDropManager are not pre-registered (they have a different instantiation pattern) but can be added manually.
When to Use UDIOT Components vs External Libraries
| Scenario | Recommendation |
|---|---|
| Server-rendered HTML with SSR/SSG | UDIOT components — render() returns a plain string |
| Theme-aware styling without a separate CSS framework | UDIOT components — built-in light/dark support via ThemeService |
| Accessibility-critical forms | UDIOT Input — includes aria-invalid, aria-describedby, label association |
| Complex client-side interactions (date pickers, rich-text) | Use an external library and hydrate with a ComponentHydrator |
| Drag-and-drop sortable lists | Use application-owned browser code; root UI helpers lack a public browser-safe entry |
Basic Usage
import { createButton, createCard, createInput, componentRegistry } from '@tagna/udiot';
// Render a button
const btn = createButton({ children: 'Submit', variant: 'primary', size: 'large' });
const html = btn.render();
// List registered components
console.log(componentRegistry.list()); // [] before application registrations
// Register a custom component
componentRegistry.register({
name: 'MyWidget',
description: 'Custom widget with special behaviour',
});
Integration Notes
- Components render to HTML strings — they are not virtual DOM elements. They are designed to be composed inside SSR page
render()methods or used standalone in template engines. onInit()is called by theBaseComponentconstructor afterthis.propsis set. Service injection via@InjectProperty()is resolved before the constructor returns only when the DI container bootstraps the component. In manual instantiation,@InjectProperty()fields will beundefinedunless you set them explicitly.generateStyles()is called in the constructor (synchronously) and again wheneverThemeServicefires a change event. BecauseAIStyler.generateComponentStyles()isasync, the first synchronous call may complete before the AI response arrives; a second re-render will pick up the generated styles.- All built-in component CSS is scoped by class names (
button,card,input-group, etc.) rather than shadow DOM. Avoid global styles that override these class names.
Pitfalls
- Do not use
createButtonand similar factories in browser-side code that requires reactivity. These components produce a static HTML string; re-rendering requires callingrender()again and replacing the DOM node. ThemeServiceis an optional dependency. If it is not provided by the DI container, components fall back to'light'theme styles. Always check thatThemeServiceis registered in your DI module if you need dark-mode support.AIStyler.generateComponentStyles()is asynchronous and calls an external AI API. It should not be used in production SSR without a caching layer — each render will make a network call, significantly increasing response latency.componentRegistry.generateComponent()returns AI-generated TypeScript source code as a string. It does not register or compile the component; you must evaluate or write the string to disk yourself.