@damarkuncoro/ui-renderers
v0.1.2
Published
Contract-driven React renderers for the CDMA architecture
Maintainers
Readme
@damarkuncoro/ui-renderers
Dynamic component renderers for contract-based UI generation, enabling runtime component creation from configuration.
📦 Installation
npm install @damarkuncoro/ui-renderers @damarkuncoro/ui-core @damarkuncoro/ui-components🎯 What are Renderers?
Renderers enable dynamic UI generation based on contracts. Instead of hardcoding components, you define UI structure through configuration that gets rendered at runtime.
Key Benefits
- Dynamic UI: Generate interfaces from configuration
- Contract Compliance: Automatic validation against contracts
- Runtime Flexibility: Change UI without code redeployment
- Type Safety: Full TypeScript support with contract validation
🚀 Basic Usage
Simple Component Rendering
import { ContractRenderer } from '@damarkuncoro/ui-renderers';
import { buttonContract } from '@damarkuncoro/ui-core/contracts';
function DynamicButton() {
const runtimeProps = {
variant: 'primary',
size: 'md',
children: 'Click me',
onClick: () => console.log('Button clicked!')
};
return (
<ContractRenderer
contract={buttonContract}
runtimeProps={runtimeProps}
/>
);
}Form Generation
import { ContractRenderer } from '@damarkuncoro/ui-renderers';
import { formContract } from '@damarkuncoro/ui-core/contracts';
const formConfig = {
fields: [
{
contract: 'input',
props: {
name: 'email',
type: 'email',
label: 'Email Address',
required: true,
placeholder: 'Enter your email'
}
},
{
contract: 'input',
props: {
name: 'password',
type: 'password',
label: 'Password',
required: true
}
},
{
contract: 'button',
props: {
type: 'submit',
variant: 'primary',
children: 'Login'
}
}
]
};
function DynamicForm() {
return (
<ContractRenderer
contract={formContract}
runtimeProps={formConfig}
/>
);
}🏗️ Architecture
Contract-Based Rendering
Contract Definition → Renderer Map → Component Instance
↓ ↓ ↓
Validation → Resolution → RenderingRenderer Resolution
// Renderer map connects contracts to components
const rendererMap = {
'ui-button': ButtonRenderer,
'ui-input': InputRenderer,
'ui-card': CardRenderer,
// ... more mappings
};📋 Advanced Usage
Custom Renderers
import { createRenderer } from '@damarkuncoro/ui-renderers';
// Create custom renderer
const CustomCardRenderer = createRenderer({
contractId: 'custom-card',
component: ({ title, content, ...props }) => (
<div className="custom-card" {...props}>
<h3 className="custom-title">{title}</h3>
<div className="custom-content">{content}</div>
</div>
),
validate: (props) => {
// Custom validation logic
if (!props.title) {
throw new Error('Title is required for custom card');
}
}
});
// Register custom renderer
registerRenderer('custom-card', CustomCardRenderer);Dynamic Page Generation
import { PageRenderer } from '@damarkuncoro/ui-renderers';
const pageConfig = {
layout: 'dashboard',
sections: [
{
type: 'stats',
components: [
{ contract: 'stats-card', props: { title: 'Revenue', value: '$45,231' } },
{ contract: 'stats-card', props: { title: 'Users', value: '12,234' } }
]
},
{
type: 'content',
components: [
{ contract: 'table', props: { data: tableData, columns: tableColumns } }
]
}
]
};
function DynamicPage() {
return <PageRenderer config={pageConfig} />;
}Conditional Rendering
const conditionalConfig = {
condition: 'user.role === "admin"',
components: [
{
contract: 'button',
props: { children: 'Admin Panel', variant: 'primary' }
}
],
fallback: {
contract: 'text',
props: { children: 'Access denied' }
}
};
<ConditionalRenderer config={conditionalConfig} context={{ user }} />🔧 Renderer API
ContractRenderer
interface ContractRendererProps {
contract: ContractEntity;
runtimeProps?: Record<string, any>;
context?: Record<string, any>;
onError?: (error: Error) => void;
onSuccess?: (result: any) => void;
}PageRenderer
interface PageRendererProps {
config: PageConfig;
context?: Record<string, any>;
theme?: string;
skin?: string;
}
interface PageConfig {
layout?: string;
sections: SectionConfig[];
}
interface SectionConfig {
type: string;
components: ComponentConfig[];
}
interface ComponentConfig {
contract: string;
props: Record<string, any>;
condition?: string;
}🎨 Integration with Themes
Theme-Aware Rendering
import { ThemeProvider } from '@damarkuncoro/ui-themes';
import { ContractRenderer } from '@damarkuncoro/ui-renderers';
function ThemedDynamicComponent() {
return (
<ThemeProvider skin="metronic">
<ContractRenderer
contract={componentContract}
runtimeProps={componentProps}
/>
</ThemeProvider>
);
}Skin-Specific Rendering
// Renderer can adapt based on active theme
const adaptiveRenderer = ({ contract, runtimeProps, theme }) => {
const skin = theme.skin || 'default';
// Different rendering logic per skin
switch (skin) {
case 'metronic':
return <MetronicComponent {...runtimeProps} />;
case 'material':
return <MaterialComponent {...runtimeProps} />;
default:
return <DefaultComponent {...runtimeProps} />;
}
};🔒 Validation & Error Handling
Contract Validation
import { validateContractProps } from '@damarkuncoro/ui-renderers';
const contract = buttonContract;
const runtimeProps = { variant: 'invalid-variant' };
try {
const validatedProps = validateContractProps(contract, runtimeProps);
// Render with validated props
} catch (error) {
console.error('Validation failed:', error.message);
// Fallback rendering or error display
}Error Boundaries
import { RendererErrorBoundary } from '@damarkuncoro/ui-renderers';
function SafeRenderer({ contract, runtimeProps }) {
return (
<RendererErrorBoundary
fallback={(error) => (
<div className="error-fallback">
Failed to render component: {error.message}
</div>
)}
>
<ContractRenderer
contract={contract}
runtimeProps={runtimeProps}
/>
</RendererErrorBoundary>
);
}📊 Performance Optimization
Memoization
import { memoizedRenderer } from '@damarkuncoro/ui-renderers';
// Automatically memoizes rendered components
const MemoizedContractRenderer = memoizedRenderer(ContractRenderer);Lazy Loading
import { lazyRenderer } from '@damarkuncoro/ui-renderers';
// Load components on demand
const LazyButtonRenderer = lazyRenderer(() =>
import('./ButtonRenderer')
);Renderer Caching
import { createRendererCache } from '@damarkuncoro/ui-renderers';
const rendererCache = createRendererCache();
// Cache rendered components
const cachedRenderer = rendererCache.get(contract.id) ||
rendererCache.set(contract.id, createRenderer(contract));🧪 Testing Renderers
Unit Testing
import { render, screen } from '@testing-library/react';
import { ContractRenderer } from '@damarkuncoro/ui-renderers';
import { buttonContract } from '@damarkuncoro/ui-core/contracts';
test('renders button from contract', () => {
const runtimeProps = {
children: 'Test Button',
onClick: jest.fn()
};
render(
<ContractRenderer
contract={buttonContract}
runtimeProps={runtimeProps}
/>
);
const button = screen.getByRole('button', { name: 'Test Button' });
expect(button).toBeInTheDocument();
});Integration Testing
import { render, fireEvent, waitFor } from '@testing-library/react';
import { FormRenderer } from '@damarkuncoro/ui-renderers';
test('form submission works', async () => {
const onSubmit = jest.fn();
const formConfig = {
fields: [
{ contract: 'input', props: { name: 'email', required: true } },
{ contract: 'button', props: { type: 'submit', children: 'Submit' } }
],
onSubmit
};
render(<FormRenderer config={formConfig} />);
const emailInput = screen.getByLabelText('email');
const submitButton = screen.getByRole('button', { name: 'Submit' });
fireEvent.change(emailInput, { target: { value: '[email protected]' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]'
});
});
});🔧 Advanced Features
Custom Renderer Plugins
import { createRendererPlugin } from '@damarkuncoro/ui-renderers';
// Create analytics plugin
const analyticsPlugin = createRendererPlugin({
name: 'analytics',
beforeRender: (contract, props) => {
// Track component render
analytics.track('component_render', {
contractId: contract.id,
props: Object.keys(props)
});
},
afterRender: (contract, element) => {
// Add tracking attributes
return React.cloneElement(element, {
'data-analytics-id': contract.id
});
}
});
// Register plugin
registerRendererPlugin(analyticsPlugin);Internationalization (i18n)
import { createI18nRenderer } from '@damarkuncoro/ui-renderers';
const i18nRenderer = createI18nRenderer({
locale: 'id',
translations: {
'button.submit': 'Kirim',
'input.placeholder': 'Masukkan teks...'
}
});
// Use i18n-aware renderer
<i18nRenderer
contract={buttonContract}
runtimeProps={{
children: 'button.submit', // Will be translated
placeholder: 'input.placeholder'
}}
/>Server-Side Rendering (SSR)
import { renderToString } from 'react-dom/server';
import { SSRContractRenderer } from '@damarkuncoro/ui-renderers';
function renderPage(config) {
const html = renderToString(
<SSRContractRenderer config={config} />
);
return `
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`;
}📚 Use Cases
CMS-Driven Interfaces
// Content Management System integration
const cmsConfig = await fetch('/api/page-config').then(r => r.json());
function CMSPage() {
return (
<PageRenderer
config={cmsConfig}
context={{ user, permissions }}
/>
);
}A/B Testing Components
// Dynamic component testing
const experimentConfig = {
experimentId: 'button-color-test',
variants: {
control: { contract: 'button', props: { variant: 'primary' } },
variant1: { contract: 'button', props: { variant: 'success' } }
}
};
<ExperimentRenderer config={experimentConfig} />Progressive Web Apps (PWA)
// Offline-capable dynamic UI
const offlineConfig = {
fallback: {
contract: 'offline-message',
props: { message: 'You are currently offline' }
},
components: isOnline ? onlineComponents : offlineComponents
};
<ProgressiveRenderer config={offlineConfig} />🔄 Migration Guide
From Static to Dynamic Rendering
// Before: Static component
function UserProfile({ user }) {
return (
<Card>
<Avatar src={user.avatar} />
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
</Card>
);
}
// After: Contract-based rendering
const userProfileConfig = {
contract: 'card',
props: {
children: [
{
contract: 'avatar',
props: { src: '{{user.avatar}}' }
},
{
contract: 'stack',
props: {
children: [
{ contract: 'text', props: { variant: 'h3', children: '{{user.name}}' } },
{ contract: 'text', props: { children: '{{user.email}}' } }
]
}
}
]
}
};
function DynamicUserProfile({ user }) {
return (
<ContractRenderer
contract={cardContract}
runtimeProps={userProfileConfig.props}
context={{ user }}
/>
);
}🤝 Contributing
When contributing to renderers:
- Performance: Optimize rendering logic
- Error Handling: Comprehensive error boundaries
- Type Safety: Full TypeScript coverage
- Testing: Both unit and integration tests
- Documentation: Include usage examples
📄 License
MIT License - see LICENSE file for details.
