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

  1. Always use key prop for dynamic lists
  2. Handle empty states gracefully
  3. Use reactive functions for data-driven children
  4. Consider performance with large datasets
  5. Separate data logic from presentation
  6. Use meaningful state paths for clarity

❌ Don'ts

  1. Don't call getState outside reactive functions for dynamic data
  2. Don't mutate arrays directly - always create new arrays
  3. Don't ignore empty states or error conditions
  4. Don't over-subscribe to rarely changing data
  5. Don't inline complex logic - extract to helper functions
  6. 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.