-
Notifications
You must be signed in to change notification settings - Fork 8
Object VDOM ‐ Children Data‐Driven Composition
Complete Guide: From basic arrays to advanced dynamic composition patterns
Data-driven children composition is the heart of dynamic UIs in Juris:
- Reactive Lists: UI automatically updates when data changes
- Dynamic Content: Components adapt to different data shapes
- Performance: Only re-renders when actual data changes occur
- Scalability: Handle lists of any size with consistent patterns
- Maintainability: Separate data logic from presentation logic
Key Insight: In Juris, children: () => {} functions create reactive bindings that automatically track data changes.
Start with the basics - master simple array mapping and static children first. As you become more comfortable with JavaScript data structures (arrays, objects, filtering, sorting), you'll naturally progress to advanced patterns. Juris provides powerful data handling capabilities, but build your confidence step by step:
- Beginners: Focus on basic mapping and static content
- Intermediate: Add filtering, sorting, and conditional rendering
- Advanced: Tackle virtual scrolling, complex grouping, and performance optimization
The framework grows with your skills - start simple, then unlock more sophisticated patterns as your JavaScript data manipulation improves.
{ div: { className: 'static-container', children: [ { h1: { text: 'Welcome' } }, { p: { text: 'This is static content' } }, { button: { text: 'Click Me' } } ] }}
{ ul: { className: 'user-list', children: () => { const users = getState('users.list', []); return users.map(user => ({ li: { key: user.id, text: user.name }})); } }} //ul.user-list
{ div: { className: 'dashboard', children: [ { header: { text: 'Dashboard' } }, //static { div: { className: 'widgets', children: () => { const widgets = getState('dashboard.widgets', []); return widgets.map(widget => ({ div: { key: widget.id, className: `widget ${widget.type}`, text: widget.title }})); } }}, //div.widgets { footer: { text: '© 2024' } } //static ] }}
{ div: { children: () => { const items = getState('data.items', []); //reactive binding return items.map(item => ({ span: { key: item.id, text: item.name }})); } }}
{ div: { children: () => { const isLoggedIn = getState('user.isLoggedIn', false); const user = getState('user.current', null); if (!isLoggedIn) { return [{ div: { text: 'Please log in' } }]; } return [ { h2: { text: `Welcome, ${user.name}` } }, { div: { text: `Last login: ${user.lastLogin}` } } ]; } }}
{ div: { children: () => { const items = getState('list.items', []); if (items.length === 0) { return [{ div: { className: 'empty-state', text: 'No items found' }}]; } return items.map(item => ({ div: { key: item.id, text: item.title }})); } }}
{ div: { className: 'product-grid', children: () => { const products = getState('products.list', []); const viewMode = getState('ui.viewMode', 'grid'); return products.map(product => { switch (viewMode) { case 'list': return { ProductListItem: { key: product.id, product } }; case 'card': return { ProductCard: { key: product.id, product } }; default: return { ProductGridItem: { key: product.id, product } }; } }); } }} //div.product-grid
{ div: { className: 'grouped-content', children: () => { const items = getState('data.items', []); const groupBy = getState('ui.groupBy', 'category'); // Group items by specified field const grouped = items.reduce((acc, item) => { const key = item[groupBy] || 'Other'; if (!acc[key]) acc[key] = []; acc[key].push(item); return acc; }, {}); return Object.entries(grouped).map(([groupName, groupItems]) => ({ div: { key: groupName, className: 'group', children: [ { h3: { text: groupName } }, { div: { className: 'group-items', children: groupItems.map(item => ({ div: { key: item.id, text: item.name }})) }} //div.group-items ] }})); } }} //div.grouped-content
{ div: { className: 'filtered-list', children: () => { const items = getState('data.items', []); const filter = getState('ui.filter', ''); const sortBy = getState('ui.sortBy', 'name'); const sortOrder = getState('ui.sortOrder', 'asc'); // Filter items let filtered = items; if (filter) { filtered = items.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()) || item.description.toLowerCase().includes(filter.toLowerCase()) ); } // Sort items const sorted = [...filtered].sort((a, b) => { let aVal = a[sortBy]; let bVal = b[sortBy]; if (typeof aVal === 'string') { aVal = aVal.toLowerCase(); bVal = bVal.toLowerCase(); } const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; return sortOrder === 'desc' ? -comparison : comparison; }); return sorted.map(item => ({ div: { key: item.id, className: 'list-item', children: [ { h4: { text: item.name } }, { p: { text: item.description } } ] }})); } }} //div.filtered-list
{ div: { className: 'paginated-list', children: () => { const items = getState('data.items', []); const currentPage = getState('ui.currentPage', 0); const pageSize = getState('ui.pageSize', 10, false); //no subscription to pageSize const startIndex = currentPage * pageSize; const endIndex = startIndex + pageSize; const pageItems = items.slice(startIndex, endIndex); return pageItems.map(item => ({ div: { key: item.id, text: item.name }})); } }}
{ div: { className: 'virtual-scroll', style: { height: '400px', overflowY: 'auto' }, children: () => { const items = getState('data.items', []); const scrollTop = getState('ui.scrollTop', 0); const itemHeight = getState('ui.itemHeight', 50, false); const containerHeight = getState('ui.containerHeight', 400, false); // Calculate visible range const startIndex = Math.floor(scrollTop / itemHeight); const visibleCount = Math.ceil(containerHeight / itemHeight); const endIndex = Math.min(startIndex + visibleCount + 2, items.length); //buffer const visibleItems = items.slice(startIndex, endIndex); return [ { div: { style: { height: `${startIndex * itemHeight}px` } //spacer before }}, ...visibleItems.map((item, index) => ({ div: { key: item.id, style: { height: `${itemHeight}px` }, text: item.name }})), { div: { style: { height: `${(items.length - endIndex) * itemHeight}px` } //spacer after }} ]; } }} //div.virtual-scroll
{ div: { className: 'async-list', children: () => { const items = getState('data.items', []); const loading = getState('data.loading', false); const error = getState('data.error', null); if (loading) { return [{ div: { className: 'loading', text: 'Loading items...' }}]; } if (error) { return [{ div: { className: 'error', children: [ { p: { text: `Error: ${error}` } }, { button: { text: 'Retry', onclick: () => loadData() }} ] }}]; } if (items.length === 0) { return [{ div: { className: 'empty', text: 'No items available' }}]; } return items.map(item => ({ div: { key: item.id, text: item.name }})); } }} //div.async-list
{ div: { className: 'progressive-list', children: () => { const items = getState('data.items', []); const hasMore = getState('data.hasMore', false); const loadingMore = getState('data.loadingMore', false); const children = items.map(item => ({ div: { key: item.id, text: item.name }})); if (loadingMore) { children.push({ div: { key: 'loading-more', className: 'loading-more', text: 'Loading more...' }}); } else if (hasMore) { children.push({ button: { key: 'load-more', className: 'load-more', text: 'Load More', onclick: () => loadMoreItems() }}); } return children; } }} //div.progressive-list
const TreeNode = (props, context) => { const { getState, setState } = context; return { div: { className: 'tree-node', children: [ { div: { className: 'node-header', children: [ { button: { className: 'toggle', text: () => getState(`tree.${props.nodeId}.expanded`, false) ? '-' : '+', onclick: () => { const expanded = getState(`tree.${props.nodeId}.expanded`, false); setState(`tree.${props.nodeId}.expanded`, !expanded); } }}, { span: { text: props.node.name } } ] }}, //div.node-header { div: { className: 'node-children', style: () => ({ display: getState(`tree.${props.nodeId}.expanded`, false) ? 'block' : 'none' }), children: () => { const children = getState(`tree.${props.nodeId}.children`, []); return children.map(child => ({ TreeNode: { key: child.id, nodeId: child.id, node: child }})); } }} //div.node-children ] } //div.tree-node }; //return };
{ form: { className: 'dynamic-form', children: () => { const schema = getState('form.schema', []); const values = getState('form.values', {}); return schema.map(field => { switch (field.type) { case 'text': return { div: { key: field.name, className: 'form-field', children: [ { label: { text: field.label } }, { input: { type: 'text', name: field.name, value: () => getState(`form.values.${field.name}`, ''), oninput: (e) => setState(`form.values.${field.name}`, e.target.value) }} ] }}; case 'select': return { div: { key: field.name, className: 'form-field', children: [ { label: { text: field.label } }, { select: { name: field.name, value: () => getState(`form.values.${field.name}`, ''), onchange: (e) => setState(`form.values.${field.name}`, e.target.value), children: field.options.map(option => ({ option: { key: option.value, value: option.value, text: option.label }})) }} ] }}; case 'checkbox': return { div: { key: field.name, className: 'form-field checkbox', children: [ { input: { type: 'checkbox', name: field.name, checked: () => getState(`form.values.${field.name}`, false), onchange: (e) => setState(`form.values.${field.name}`, e.target.checked) }}, { label: { text: field.label } } ] }}; default: return { div: { key: field.name, text: `Unknown field type: ${field.type}` }}; } }); } }} //form.dynamic-form
{ div: { className: 'layout-container', children: () => { const layout = getState('ui.layout', 'grid'); const items = getState('data.items', []); const columns = getState('ui.columns', 3, false); switch (layout) { case 'grid': return [{ div: { className: 'grid-layout', style: { gridTemplateColumns: `repeat(${columns}, 1fr)` }, children: items.map(item => ({ div: { key: item.id, className: 'grid-item', text: item.name }})) }}]; case 'list': return [{ div: { className: 'list-layout', children: items.map(item => ({ div: { key: item.id, className: 'list-item', children: [ { h4: { text: item.name } }, { p: { text: item.description } } ] }})) }}]; case 'masonry': // Group items into columns const itemColumns = Array.from({ length: columns }, () => []); items.forEach((item, index) => { itemColumns[index % columns].push(item); }); return [{ div: { className: 'masonry-layout', children: itemColumns.map((columnItems, columnIndex) => ({ div: { key: columnIndex, className: 'masonry-column', children: columnItems.map(item => ({ div: { key: item.id, className: 'masonry-item', text: item.name }})) }})) }}]; default: return [{ div: { text: 'Unknown layout type' } }]; } } }} //div.layout-container
{ div: { children: () => { const items = getState('data.items', []); const filter = getState('ui.filter', ''); // Create cache key from dependencies const cacheKey = `filtered_${items.length}_${filter}`; const cached = getState(`cache.${cacheKey}`, null, false); if (cached) { return cached; } // Expensive computation const filtered = items.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()) ); const result = filtered.map(item => ({ div: { key: item.id, text: item.name }})); // Cache result (no subscription to prevent loops) setState(`cache.${cacheKey}`, result); return result; } }}
{ div: { children: () => { // Read frequently changing data const activeTab = getState('ui.activeTab', 'all'); // Read less frequently changing data without subscription const allItems = getState('data.items', [], false); const categories = getState('data.categories', [], false); // Only re-render when activeTab changes, not when items change const filteredItems = activeTab === 'all' ? allItems : allItems.filter(item => item.category === activeTab); return filteredItems.map(item => ({ div: { key: item.id, text: item.name }})); } }}
-
Always use
keyprop for dynamic lists - Handle empty states gracefully
- Use reactive functions for data-driven children
- Consider performance with large datasets
- Separate data logic from presentation
- Use meaningful state paths for clarity
-
Don't call
getStateoutside reactive functions for dynamic data - Don't mutate arrays directly - always create new arrays
- Don't ignore empty states or error conditions
- Don't over-subscribe to rarely changing data
- Don't inline complex logic - extract to helper functions
- Don't forget cleanup for expensive computations
-
Static children:
children: [...] -
Dynamic children:
children: () => data.map(...) -
Conditional children:
children: () => condition ? [...] : [...] - Mixed children: Static items + dynamic reactive section
-
Performance: Use third parameter
falsefor non-reactive reads
Following these patterns ensures your Juris applications handle data-driven UIs efficiently and maintainably.