Solid.js 示例
Solid.js 组件示例,包括钩子、状态管理、路由和现代 React 模式
💻 Solid.js 你好世界 typescript
🟢 simple
基础的 Solid.js 组件示例和使用 JSX 的你好世界应用程序
// Solid.js Hello World Examples
// 1. Basic functional component
import { createSignal } from 'solid-js';
function HelloWorld() {
return <h1>Hello, World!</h1>;
}
// 2. Component with props
interface GreetingProps {
name: string;
}
function Greeting(props: GreetingProps) {
return <h1>Hello, {props.name}!</h1>;
}
// 3. Component with signal (reactive state)
function Counter() {
const [count, setCount] = createSignal(0);
return (
<div>
<h1>Count: {count()}</h1>
<button onClick={() => setCount(count() + 1)}>
Increment
</button>
<button onClick={() => setCount(count() - 1)}>
Decrement
</button>
</div>
);
}
// 4. Component with multiple signals
function UserProfile() {
const [name, setName] = createSignal('Guest');
const [age, setAge] = createSignal(25);
const [isLoggedIn, setIsLoggedIn] = createSignal(false);
return (
<div>
<h2>User Profile</h2>
{isLoggedIn() ? (
<div>
<p>Name: {name()}</p>
<p>Age: {age()}</p>
<input
type="text"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
placeholder="Enter name"
/>
<input
type="number"
value={age()}
onInput={(e) => setAge(Number(e.currentTarget.value))}
placeholder="Enter age"
/>
<button onClick={() => setIsLoggedIn(false)}>Logout</button>
</div>
) : (
<button onClick={() => setIsLoggedIn(true)}>Login</button>
)}
</div>
);
}
// 5. Component with derived values
function ShoppingCart() {
const [items, setItems] = createSignal([
{ name: 'Apple', price: 1.5, quantity: 2 },
{ name: 'Banana', price: 0.8, quantity: 5 }
]);
const total = () => items().reduce((sum, item) => sum + (item.price * item.quantity), 0);
const itemCount = () => items().reduce((sum, item) => sum + item.quantity, 0);
return (
<div>
<h2>Shopping Cart</h2>
<p>Items: {itemCount()}</p>
<p>Total: ${total().toFixed(2)}</p>
<ul>
{items().map((item, index) => (
<li>
{item.name} - ${item.price} x {item.quantity}
<button
onClick={() => {
const newItems = [...items()];
newItems.splice(index, 1);
setItems(newItems);
}}
>
Remove
</button>
</li>
))}
</ul>
</div>
);
}
// Usage examples
function App() {
return (
<div>
<HelloWorld />
<Greeting name="Solid.js" />
<Counter />
<UserProfile />
<ShoppingCart />
</div>
);
}
export { HelloWorld, Greeting, Counter, UserProfile, ShoppingCart, App };
💻 Solid.js 信号和效果 typescript
🟡 intermediate
在 Solid.js 中使用响应式信号、效果和派生值
// Solid.js Signals and Effects
import {
createSignal,
createEffect,
createMemo,
createComputed,
onMount,
onCleanup,
batch
} from 'solid-js';
// 1. Basic signals and effects
function SignalExample() {
const [count, setCount] = createSignal(0);
const [doubled, setDoubled] = createSignal(0);
// Effect that runs when count changes
createEffect(() => {
console.log('Count changed to:', count());
setDoubled(count() * 2);
});
// Effect with cleanup
createEffect((prevCount) => {
console.log('Previous count:', prevCount);
return count();
});
return (
<div>
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<button onClick={() => setCount(count() + 1)}>Increment</button>
</div>
);
}
// 2. Memo (derived values)
function MemoExample() {
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
// Memoized computed value
const fullName = createMemo(() => `${firstName()} ${lastName()}`);
const greeting = createMemo(() => `Hello, ${fullName()}!`);
return (
<div>
<input
type="text"
value={firstName()}
onInput={(e) => setFirstName(e.currentTarget.value)}
placeholder="First name"
/>
<input
type="text"
value={lastName()}
onInput={(e) => setLastName(e.currentTarget.value)}
placeholder="Last name"
/>
<p>{greeting()}</p>
</div>
);
}
// 3. Lifecycle effects
function LifecycleExample() {
const [data, setData] = createSignal(null);
const [loading, setLoading] = createSignal(true);
onMount(() => {
console.log('Component mounted');
// Simulate API call
setTimeout(() => {
setData({ message: 'Data loaded successfully!' });
setLoading(false);
}, 2000);
});
onCleanup(() => {
console.log('Component will unmount');
});
// Effect for data changes
createEffect(() => {
if (data()) {
console.log('Data updated:', data());
}
});
return (
<div>
<h3>Lifecycle Example</h3>
{loading() ? (
<p>Loading...</p>
) : (
<p>{data()?.message}</p>
)}
</div>
);
}
// 4. Batch updates for performance
function BatchExample() {
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('');
const [isValid, setIsValid] = createSignal(false);
// Effect that runs only once when batched
createEffect(() => {
console.log('Count:', count(), 'Name:', name(), 'Valid:', isValid());
});
const updateMultiple = () => {
batch(() => {
setCount(count() + 1);
setName('Updated');
setIsValid(true);
});
};
return (
<div>
<p>Count: {count()}</p>
<p>Name: {name()}</p>
<p>Valid: {isValid() ? 'Yes' : 'No'}</p>
<button onClick={updateMultiple}>Update Multiple (Batched)</button>
</div>
);
}
// 5. Computed signals
function ComputedExample() {
const [price, setPrice] = createSignal(100);
const [quantity, setQuantity] = createSignal(2);
const [taxRate] = createSignal(0.08);
// Computed signal
const subtotal = createComputed(() => price() * quantity());
const tax = createComputed(() => subtotal() * taxRate());
const total = createComputed(() => subtotal() + tax());
return (
<div>
<h3>Price Calculator</h3>
<label>
Price: $
<input
type="number"
value={price()}
onInput={(e) => setPrice(Number(e.currentTarget.value))}
/>
</label>
<label>
Quantity:
<input
type="number"
value={quantity()}
onInput={(e) => setQuantity(Number(e.currentTarget.value))}
/>
</label>
<p>Subtotal: ${subtotal().toFixed(2)}</p>
<p>Tax ({taxRate() * 100}%): ${tax().toFixed(2)}</p>
<p><strong>Total: ${total().toFixed(2)}</strong></p>
</div>
);
}
export { SignalExample, MemoExample, LifecycleExample, BatchExample, ComputedExample };
💻 Solid.js 存储 typescript
🟡 intermediate
使用 Solid.js 存储进行全局状态管理
// Solid.js Stores for Global State Management
import { createStore, SetStoreFunction } from 'solid-js/store';
// 1. Simple store
interface User {
id: number;
name: string;
email: string;
isLoggedIn: boolean;
}
const [user, setUser] = createStore<User>({
id: 0,
name: '',
email: '',
isLoggedIn: false
});
// Store actions
const userActions = {
login: (userData: Omit<User, 'isLoggedIn'>) => {
setUser({
...userData,
isLoggedIn: true
});
},
logout: () => {
setUser({
id: 0,
name: '',
email: '',
isLoggedIn: false
});
},
updateProfile: (updates: Partial<Pick<User, 'name' | 'email'>>) => {
setUser(updates);
}
};
// 2. Todo store
interface Todo {
id: number;
text: string;
completed: boolean;
createdAt: Date;
}
interface TodoStore {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
}
const [todoStore, setTodoStore] = createStore<TodoStore>({
todos: [],
filter: 'all'
});
const todoActions = {
addTodo: (text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false,
createdAt: new Date()
};
setTodoStore('todos', todos => [...todos, newTodo]);
},
toggleTodo: (id: number) => {
setTodoStore('todos',
todo => todo.id === id,
'completed',
completed => !completed
);
},
deleteTodo: (id: number) => {
setTodoStore('todos', todos => todos.filter(todo => todo.id !== id));
},
clearCompleted: () => {
setTodoStore('todos', todos => todos.filter(todo => !todo.completed));
},
setFilter: (filter: 'all' | 'active' | 'completed') => {
setTodoStore('filter', filter);
},
// Getters
get activeTodos() {
return todoStore.todos.filter(todo => !todo.completed);
},
get completedTodos() {
return todoStore.todos.filter(todo => todo.completed);
},
get filteredTodos() {
switch (todoStore.filter) {
case 'active':
return todoStore.todos.filter(todo => !todo.completed);
case 'completed':
return todoStore.todos.filter(todo => todo.completed);
default:
return todoStore.todos;
}
},
get stats() {
const total = todoStore.todos.length;
const completed = todoStore.todos.filter(todo => todo.completed).length;
const active = total - completed;
return { total, completed, active };
}
};
// 3. Shopping cart store
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
isOpen: boolean;
couponCode?: string;
discount: number;
}
const [cartStore, setCartStore] = createStore<CartStore>({
items: [],
isOpen: false,
discount: 0
});
const cartActions = {
addItem: (item: Omit<CartItem, 'quantity'>) => {
setCartStore('items', items => {
const existingItem = items.find(i => i.id === item.id);
if (existingItem) {
return items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { ...item, quantity: 1 }];
});
},
removeItem: (id: number) => {
setCartStore('items', items => items.filter(item => item.id !== id));
},
updateQuantity: (id: number, quantity: number) => {
if (quantity <= 0) {
cartActions.removeItem(id);
} else {
setCartStore('items',
item => item.id === id,
'quantity',
quantity
);
}
},
clearCart: () => {
setCartStore('items', []);
},
toggleCart: () => {
setCartStore('isOpen', isOpen => !isOpen);
},
applyCoupon: (code: string, discount: number) => {
setCartStore('couponCode', code);
setCartStore('discount', discount);
}
};
// Cart computed values
const cartHelpers = {
get totalItems() {
return cartStore.items.reduce((sum, item) => sum + item.quantity, 0);
},
get subtotal() {
return cartStore.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
get total() {
const subtotal = this.subtotal;
return subtotal - (subtotal * cartStore.discount);
}
};
// 4. Store with nested state
interface Product {
id: number;
name: string;
price: number;
category: string;
inStock: boolean;
}
interface ProductsStore {
products: Product[];
categories: string[];
selectedCategory: string;
searchTerm: string;
sortBy: 'name' | 'price';
sortOrder: 'asc' | 'desc';
}
const [productsStore, setProductsStore] = createStore<ProductsStore>({
products: [],
categories: [],
selectedCategory: '',
searchTerm: '',
sortBy: 'name',
sortOrder: 'asc'
});
const productActions = {
setProducts: (products: Product[]) => {
setProductsStore('products', products);
// Extract categories
const categories = [...new Set(products.map(p => p.category))];
setProductsStore('categories', categories);
},
setCategory: (category: string) => {
setProductsStore('selectedCategory', category);
},
setSearchTerm: (term: string) => {
setProductsStore('searchTerm', term);
},
setSorting: (sortBy: 'name' | 'price', sortOrder: 'asc' | 'desc') => {
setProductsStore('sortBy', sortBy);
setProductsStore('sortOrder', sortOrder);
}
};
export {
user, userActions,
todoStore, todoActions,
cartStore, cartActions, cartHelpers,
productsStore, productActions
};
💻 Solid.js 路由 typescript
🟡 intermediate
使用 Solid Router 进行客户端路由
// Solid.js Routing Examples
import {
createRouter,
createRoute,
createMemoryHistory,
Router,
Route,
Link,
useParams,
useSearchParams,
useNavigate,
A
} from '@solidjs/router';
// 1. Basic router setup
const router = createRouter({
history: createMemoryHistory(),
routes: [
createRoute({
path: '/',
component: () => import('./pages/Home')
}),
createRoute({
path: '/about',
component: () => import('./pages/About')
}),
createRoute({
path: '/users/:id',
component: () => import('./pages/UserDetail')
}),
createRoute({
path: '/search',
component: () => import('./pages/Search')
})
]
});
// 2. Navigation components
function Navigation() {
const navigate = useNavigate();
const [params] = useSearchParams();
const handleGoHome = () => navigate('/');
const handleGoBack = () => navigate(-1);
return (
<nav>
<ul>
<li><A href="/">Home</A></li>
<li><A href="/about">About</A></li>
<li><A href="/users/1">User 1</A></li>
<li><A href="/search?q=test">Search</A></li>
</ul>
<div>
<button onClick={handleGoHome}>Go Home</button>
<button onClick={handleGoBack}>Go Back</button>
<p>Current query: {params.q}</p>
</div>
</nav>
);
}
// 3. Dynamic routes
function UserDetail() {
const params = useParams();
const [searchParams] = useSearchParams();
return (
<div>
<h2>User Detail</h2>
<p>User ID: {params.id}</p>
<p>Tab: {searchParams.tab || 'profile'}</p>
<div class="user-tabs">
<A href={`/users/${params.id}?tab=profile`}>Profile</A>
<A href={`/users/${params.id}?tab=settings`}>Settings</A>
<A href={`/users/${params.id}?tab=posts`}>Posts</A>
</div>
<div class="tab-content">
<Show when={searchParams.tab === 'profile'}>
<UserProfile id={params.id} />
</Show>
<Show when={searchParams.tab === 'settings'}>
<UserSettings id={params.id} />
</Show>
<Show when={searchParams.tab === 'posts'}>
<UserPosts id={params.id} />
</Show>
</div>
</div>
);
}
// 4. Search functionality
function SearchPage() {
const [params, setParams] = useSearchParams();
const navigate = useNavigate();
const handleSearch = (query: string) => {
setParams({ q: query });
};
const handleAdvancedSearch = (query: string, filters: any) => {
const searchParams = new URLSearchParams();
searchParams.set('q', query);
Object.entries(filters).forEach(([key, value]) => {
if (value) searchParams.set(key, String(value));
});
navigate(`/search?${searchParams.toString()}`);
};
return (
<div>
<h2>Search</h2>
<div class="search-form">
<input
type="text"
placeholder="Search..."
value={params.q || ''}
onInput={(e) => handleSearch(e.currentTarget.value)}
/>
</div>
<Show when={params.q}>
<div class="search-results">
<p>Searching for: {params.q}</p>
<SearchResults query={params.q} />
</div>
</Show>
</div>
);
}
// 5. Protected routes
interface ProtectedRouteProps {
children: any;
redirectTo?: string;
}
function ProtectedRoute(props: ProtectedRouteProps) {
const isAuthenticated = true; // Replace with actual auth logic
const navigate = useNavigate();
createEffect(() => {
if (!isAuthenticated) {
navigate(props.redirectTo || '/login');
}
});
return <Show when={isAuthenticated}>{props.children}</Show>;
}
// 6. Route-based code splitting
function LazyPage() {
const LazyComponent = lazy(() => import('./components/HeavyComponent'));
return (
<Suspense fallback={<div>Loading page...</div>}>
<LazyComponent />
</Suspense>
);
}
// 7. Nested routes
function App() {
return (
<Router>
<div class="app">
<Navigation />
<main>
<Route path="/" component={HomePage} />
<Route path="/about" component={AboutPage} />
<ProtectedRoute>
<Route path="/dashboard/*" component={DashboardLayout}>
<Route path="/dashboard" component={DashboardHome} />
<Route path="/dashboard/users" component={UserManagement} />
<Route path="/dashboard/settings" component={Settings} />
</Route>
</ProtectedRoute>
<Route path="/users/:id" component={UserDetail} />
<Route path="/search" component={SearchPage} />
<Route path="*" component={NotFoundPage} />
</main>
</div>
</Router>
);
}
// 8. Page components
function HomePage() {
return (
<div>
<h1>Home Page</h1>
<p>Welcome to the Solid.js app!</p>
</div>
);
}
function AboutPage() {
return (
<div>
<h1>About Page</h1>
<p>This is an about page with routing.</p>
</div>
);
}
function DashboardLayout(props: any) {
return (
<div class="dashboard">
<aside>
<h3>Dashboard</h3>
<nav>
<A href="/dashboard">Overview</A>
<A href="/dashboard/users">Users</A>
<A href="/dashboard/settings">Settings</A>
</nav>
</aside>
<div class="dashboard-content">
{props.children}
</div>
</div>
);
}
function DashboardHome() {
return <div>Dashboard Overview</div>;
}
function UserManagement() {
return <div>User Management</div>;
}
function Settings() {
return <div>Settings</div>;
}
function NotFoundPage() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<A href="/">Go Home</A>
</div>
);
}
// Helper components (simplified implementations)
function UserProfile(props: { id: string }) {
return <div>User Profile for {props.id}</div>;
}
function UserSettings(props: { id: string }) {
return <div>User Settings for {props.id}</div>;
}
function UserPosts(props: { id: string }) {
return <div>User Posts for {props.id}</div>;
}
function SearchResults(props: { query: string }) {
return <div>Search results for "{props.query}"</div>;
}
export {
App,
router,
Navigation,
ProtectedRoute,
HomePage,
AboutPage,
DashboardLayout,
UserDetail,
SearchPage
};
💻 Solid.js 组件模式 typescript
🔴 complex
Solid.js 中的常见组件模式和最佳实践
// Solid.js Component Patterns
import {
createSignal,
For,
Show,
Switch,
Match,
Index,
ErrorBoundary,
Suspense,
createResource,
createContext,
useContext,
ParentComponent,
createMemo
} from 'solid-js';
// 1. List rendering patterns
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
function UserList(props: { users: User[] }) {
const [selectedUserId, setSelectedUserId] = createSignal<number | null>(null);
return (
<div>
<h3>User List</h3>
{/* Using For (efficient for dynamic lists) */}
<For each={props.users}>
{(user) => (
<div
class=`user-item ${selectedUserId() === user.id ? 'selected' : ''}`
onClick={() => setSelectedUserId(user.id)}
>
<h4>{user.name}</h4>
<p>{user.email}</p>
<span class="role">{user.role}</span>
</div>
)}
</For>
{/* Using Index (when index matters) */}
<h4>Indexed List:</h4>
<Index each={props.users}>
{(user, index) => (
<div>
{index + 1}. {user().name} - {user().role}
</div>
)}
</Index>
</div>
);
}
// 2. Conditional rendering patterns
function ConditionalRendering() {
const [status, setStatus] = createSignal<'loading' | 'success' | 'error'>('loading');
const [user, setUser] = createSignal<User | null>(null);
return (
<div>
<h3>Conditional Rendering</h3>
{/* Basic Show/Else */}
<Show when={user()} fallback={<p>Please log in</p>}>
{(user) => (
<p>Welcome, {user().name}!</p>
)}
</Show>
{/* Switch/Match for multiple conditions */}
<Switch fallback={<p>Unknown status</p>}>
<Match when={status() === 'loading'}>
<div class="spinner">Loading...</div>
</Match>
<Match when={status() === 'success'}>
<div class="success">✓ Success!</div>
</Match>
<Match when={status() === 'error'}>
<div class="error">✗ Error occurred</div>
</Match>
</Switch>
<button onClick={() => setStatus('loading')}>Set Loading</button>
<button onClick={() => setStatus('success')}>Set Success</button>
<button onClick={() => setStatus('error')}>Set Error</button>
</div>
);
}
// 3. Context pattern
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType>();
const ThemeProvider: ParentComponent = (props) => {
const [theme, setTheme] = createSignal<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(current => current === 'light' ? 'dark' : 'light');
};
const context: ThemeContextType = {
theme: theme(),
toggleTheme
};
return (
<ThemeContext.Provider value={context}>
<div class={`app ${theme()}-theme`}>
{props.children}
</div>
</ThemeContext.Provider>
);
};
const useTheme = () => useContext(ThemeContext);
function ThemedComponent() {
const { theme, toggleTheme } = useTheme();
return (
<div class={`themed-component ${theme}`}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// 4. Error boundaries
function ErrorBoundaryExample() {
const [shouldError, setShouldError] = createSignal(false);
const ThrowError = () => {
if (shouldError()) {
throw new Error('Something went wrong!');
}
return <div>No error</div>;
};
return (
<div>
<h3>Error Boundary</h3>
<button onClick={() => setShouldError(true)}>
Trigger Error
</button>
<button onClick={() => setShouldError(false)}>
Reset
</button>
<ErrorBoundary fallback={(err, reset) => (
<div class="error-boundary">
<h4>Error: {err.message}</h4>
<button onClick={reset}>Try Again</button>
</div>
)}>
<ThrowError />
</ErrorBoundary>
</div>
);
}
// 5. Data fetching with Suspense
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
async function fetchPosts(): Promise<Post[]> {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 2000));
return [
{ id: 1, title: 'Post 1', body: 'This is post 1', userId: 1 },
{ id: 2, title: 'Post 2', body: 'This is post 2', userId: 2 }
];
}
function DataFetching() {
const [posts] = createResource(fetchPosts);
return (
<div>
<h3>Data Fetching with Suspense</h3>
<Suspense fallback={<div class="spinner">Loading posts...</div>}>
<Show when={!posts.error} when={!posts.loading}>
<div class="posts">
<For each={posts()}>
{(post) => (
<article class="post">
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
)}
</For>
</div>
</Show>
</Suspense>
</div>
);
}
// 6. Higher-order component pattern
const withLoading = <P extends object>(Component: (props: P) => any) => {
return (props: P & { loading?: boolean }) => {
return (
<Show when={!props.loading} fallback={<div class="spinner">Loading...</div>}>
<Component {...props} />
</Show>
);
};
};
const UserProfile = (props: { name: string; email: string }) => (
<div class="user-profile">
<h4>{props.name}</h4>
<p>{props.email}</p>
</div>
);
const UserProfileWithLoading = withLoading(UserProfile);
// 7. Render props pattern
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: () => number) => any;
emptyMessage?: string;
}
function List<T>(props: ListProps<T>) {
return (
<div class="list">
<Show when={props.items.length > 0} fallback={
<div class="empty">{props.emptyMessage || 'No items'}</div>
}>
<For each={props.items}>
{(item, index) => props.renderItem(item, index)}
</For>
</Show>
</div>
);
}
// 8. Compound component pattern
interface TabsContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextType>();
const Tabs: ParentComponent<{ initialTab?: string }> = (props) => {
const [activeTab, setActiveTab] = createSignal(props.initialTab || '');
return (
<TabsContext.Provider value={{ activeTab: activeTab(), setActiveTab }}>
<div class="tabs">{props.children}</div>
</TabsContext.Provider>
);
};
const TabList: ParentComponent = (props) => {
return <div class="tab-list">{props.children}</div>;
};
const Tab: ParentComponent<{ value: string }> = (props) => {
const { activeTab, setActiveTab } = useContext(TabsContext)!;
const isActive = activeTab === props.value;
return (
<button
class={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveTab(props.value)}
>
{props.children}
</button>
);
};
const TabPanels: ParentComponent = (props) => {
return <div class="tab-panels">{props.children}</div>;
};
const TabPanel: ParentComponent<{ value: string }> = (props) => {
const { activeTab } = useContext(TabsContext)!;
return (
<Show when={activeTab === props.value}>
<div class="tab-panel">{props.children}</div>
</Show>
);
};
export {
UserList,
ConditionalRendering,
ThemeProvider,
ThemedComponent,
ErrorBoundaryExample,
DataFetching,
UserProfileWithLoading,
List,
Tabs,
TabList,
Tab,
TabPanels,
TabPanel
};