Object VDOM ‐ Children Data‐Driven Composition - jurisjs/juris GitHub Wiki
Juris Children Data-Driven Composition Wiki
Complete Guide: From basic arrays to advanced dynamic composition patterns
Why Data-Driven Children Matter
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.
Learning Path for Developers
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.
Basic Children Patterns
Static Children Array
{ div: {
className: 'static-container',
children: [
{ h1: { text: 'Welcome' } },
{ p: { text: 'This is static content' } },
{ button: { text: 'Click Me' } }
]
}}
Simple Data Mapping
{ ul: {
className: 'user-list',
children: () => {
const users = getState('users.list', []);
return users.map(user => ({ li: {
key: user.id,
text: user.name
}}));
}
}} //ul.user-list
Mixed Static and Dynamic
{ 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
]
}}
Reactive Children Functions
Basic Reactive Pattern
{ div: {
children: () => {
const items = getState('data.items', []); //reactive binding
return items.map(item => ({ span: {
key: item.id,
text: item.name
}}));
}
}}
Conditional Children
{ 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}` } }
];
}
}}
Empty State Handling
{ 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
}}));
}
}}
Advanced Data Composition
Nested Component Mapping
{ 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
Grouped Data Composition
{ 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
Filtered and Sorted Composition
{ 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
Pagination and Virtual Scrolling
Basic Pagination
{ 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
}}));
}
}}
Virtual Scrolling Pattern
{ 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
Async Data Composition
Loading States
{ 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
Progressive Loading
{ 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
Complex Composition Patterns
Tree Structure Rendering
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
};
Dynamic Form Fields
{ 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
Layout-Based Composition
{ 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
Performance Optimization
Memoization Pattern
{ 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;
}
}}
Selective Re-rendering
{ 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
}}));
}
}}
Best Practices Summary
✅ Do's
- Always use
key
prop 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'ts
- Don't call
getState
outside 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
Key Patterns
- Static children:
children: [...]
- Dynamic children:
children: () => data.map(...)
- Conditional children:
children: () => condition ? [...] : [...]
- Mixed children: Static items + dynamic reactive section
- Performance: Use third parameter
false
for non-reactive reads
Following these patterns ensures your Juris applications handle data-driven UIs efficiently and maintainably.