Your First Vertical
This hands-on guide walks you through building your first feature - creating a new tenant configuration. By the end, you'll understand the core concepts of config-driven UI development.
What You'll Learn
- How tenant configurations work
- The config-driven UI pattern
- Store slices and state management
- Component rendering via slots
Prerequisites
- Completed the Quickstart
- Development server running (
npm run dev)
Step 1: Understand the Template Config
Open src/eai.config/tenants/template.config.ts - this is the example tenant configuration.
// Key parts of a tenant config:
export const templateConfig: EAIConfig = {
tenantId: 'template', // Unique identifier
displayName: 'Template', // UI display name
// State management - define your app's global state
store: {
property: {
initialState: { selectedAddress: null },
persist: true, // Survives page refresh
},
ui: {
initialState: { sidebarOpen: true },
persist: false, // Resets on refresh
},
},
// Layout - which components render in which slots
layout: {
header: [...], // Top navigation
leftPane: [...], // Left sidebar
middlePane: [...], // Main content
rightPane: [...], // Right sidebar
},
};
Step 2: Create Your First Tenant
Create a new file src/eai.config/tenants/my-vertical.config.ts:
import type { EAIConfig } from '../types';
export const myVerticalConfig: EAIConfig = {
tenantId: 'my-vertical',
displayName: 'My Vertical',
// Define your application state
store: {
user: {
initialState: {
name: null,
email: null,
preferences: {},
},
persist: true,
},
ui: {
initialState: {
theme: 'light',
sidebarCollapsed: false,
},
persist: true,
},
},
// Define your UI layout
layout: {
header: [
{
component: 'Header',
priority: 1,
props: {
title: 'My Vertical',
showUserMenu: true,
},
storeBindings: [
{ prop: 'userName', storePath: 'user.name' },
],
},
],
leftPane: [],
middlePane: [
{
component: 'WelcomeCard',
priority: 1,
props: {
message: 'Welcome to My Vertical!',
},
showWhen: (state) => !state.user?.name,
},
],
rightPane: [],
},
};
Step 3: Register Your Tenant
Edit src/eai.config/index.ts to add your new config:
import { templateConfig } from './tenants/template.config';
import { myVerticalConfig } from './tenants/my-vertical.config';
const configs: Record<string, EAIConfig> = {
template: templateConfig,
'my-vertical': myVerticalConfig, // Add this line
};
export function getTenantConfig(tenantId: string): EAIConfig {
return configs[tenantId] || configs.template;
}
Step 4: Test Your Configuration
Add ?tenant=my-vertical to your URL:
http://localhost:3000?tenant=my-vertical
You should see your custom header title "My Vertical" instead of the default.
Step 5: Add Store Interaction
Let's add a component that reads and writes to the store.
Create src/components/UserForm.tsx:
'use client';
import { useStoreValue, useSetStore } from '@enterpriseaigroup/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useState } from 'react';
export function UserForm() {
const currentName = useStoreValue('user.name');
const setStore = useSetStore();
const [inputName, setInputName] = useState('');
const handleSubmit = () => {
setStore('user.name', inputName, 'UserForm');
setInputName('');
};
return (
<div className="p-4 space-y-4">
<p>Current Name: {currentName || 'Not set'}</p>
<Input
value={inputName}
onChange={(e) => setInputName(e.target.value)}
placeholder="Enter your name"
/>
<Button onClick={handleSubmit}>Save Name</Button>
</div>
);
}
Step 6: Register the Component
Add to the ComponentRegistry in packages/client/src/config/ComponentRegistry.ts:
import { UserForm } from '@/components/UserForm';
registry.set('UserForm', UserForm);
Step 7: Add to Your Layout
Update your tenant config to include the form:
middlePane: [
{
component: 'UserForm',
priority: 1,
},
{
component: 'WelcomeCard',
priority: 2,
props: {
message: 'Welcome to My Vertical!',
},
showWhen: (state) => !state.user?.name,
},
],
Understanding What Happened
- Config-Driven UI: The layout defined in your config determines what renders
- Store Slices:
useranduislices hold your app state - Store Bindings:
storeBindingsautomatically pass store values as props - Conditional Rendering:
showWhencontrols component visibility based on state - Hooks:
useStoreValuereads state,useSetStorewrites to it
Key Concepts Reference
| Concept | Purpose | Example |
|---|---|---|
tenantId | Identifies the tenant | 'my-vertical' |
store | Defines global state | { user: { initialState: {...} } } |
layout | Component placement | { header: [...], middlePane: [...] } |
storeBindings | Connect store to props | [{ prop: 'name', storePath: 'user.name' }] |
showWhen | Conditional rendering | (state) => state.user?.name |
priority | Render order (lower = first) | 1, 2, 3... |
Next Steps
You've created your first tenant configuration. Continue learning:
- Architecture - Deeper system understanding
- Configuration - Full config reference
- Adding Pages - Multi-page applications
- State Management - Advanced store patterns
Exercises
Try these to reinforce your learning:
- Add a theme toggle: Use
ui.themeto switch between light/dark - Add preferences: Store user preferences in
user.preferences - Conditional header: Show different header based on auth state
- Multiple tenants: Create another tenant with different branding