Object VDOM Conventions - jurisjs/juris GitHub Wiki

Juris Object VDOM Conventions - Quick Reference

Focus: Object VDOM structure conventions and reactivity patterns only

Why Follow These Conventions?

Following Juris Object VDOM conventions ensures an enjoyable first experience and long-term success:

  • Visual Clarity: Proper formatting makes complex nested structures instantly readable
  • Debugging Ease: End-bracket labels help you quickly identify which element closes where
  • Performance: Inline static properties vs reactive functions optimize rendering cycles
  • Team Consistency: Standardized patterns make code reviews and collaboration smoother
  • Framework Alignment: Working with Juris's reactivity system instead of fighting it

Remember: Juris is designed to make complex UIs simple. These conventions help you leverage that power effectively.


The 7 Core Rules

  1. Reactivity works when getState is called from intended functional attributes and children
  2. Use compress object structure and add labels into the end brackets for nested divs, tables, select, groups and forms
  3. Use getState third attribute with false value to skip subscription
  4. Components will not re-render until their parent triggers re-render
  5. All props and attributes can handle async/sync natively
  6. Use service injection AMAP
  7. Define component as function and don't inject directly into Juris during instantiation

Object VDOM Structure Conventions

Correct Element Formatting

return {
  div: {
    className: 'main', //note: static and short should be inline
    text: () => getState('reactive.text.value', 'Hello'), //note: reactive, should be new line
    style: { color: 'red', border: 'solid 1px blue' }, //note: still okay if in-line
    children: [
      { button: {
        text: 'static label', //note: another static and short should be inline
        onClick: () => clickHandler()
      }}, //button
      { input: {
        type: 'text', min: '1', max: '10',
        value: () => getState('counter.step', 1), //note: reactive value
        oninput: (e) => {
          const newStep = parseInt(e.target.value) || 1;
          setState('counter.step', Math.max(1, Math.min(10, newStep)));
        }
      }} //input
    ]
  } //div.main
}; //return

Key Formatting Rules:

  1. Static & Short Properties: Same line as element
  2. Reactive Properties: New line with function syntax
  3. Element Arrays: { element: { props }} format
  4. End Labels: Add //elementType.className or //elementType for identification

Property Type Patterns

Static Properties (Inline)

{ div: { className: 'container', id: 'main', tabIndex: 0 } }
{ button: { type: 'submit', disabled: true, text: 'Save' } }
{ input: { type: 'email', required: true, placeholder: 'Enter email' } }

Reactive Properties (New Line)

{ div: {
  text: () => getState('user.name', 'Anonymous'),
  className: () => `status ${getState('user.online', false) ? 'online' : 'offline'}`,
  style: () => ({ color: getState('theme.color', 'black') })
}}

Mixed Properties

{ button: {
  type: 'button', className: 'action-btn', //static inline
  disabled: () => getState('form.saving', false), //reactive new line
  text: () => getState('form.saving', false) ? 'Saving...' : 'Save', //reactive new line
  onclick: () => handleSave()
}}

Children Array Formatting

Simple Children

{ div: {
  className: 'container',
  children: [
    { h1: { text: 'Title' } },
    { p: { text: 'Description' } },
    { button: { text: 'Action' } }
  ]
}}

Complex Nested Structure

{ div: {
  className: 'layout',
  children: [
    { header: {
      className: 'app-header',
      children: [
        { h1: { text: 'App Name' } },
        { nav: {
          children: [
            { a: { href: '/', text: 'Home' } },
            { a: { href: '/about', text: 'About' } }
          ]
        }} //nav
      ]
    }}, //header.app-header
    { main: {
      className: 'content',
      children: [
        { section: {
          children: [
            { h2: { text: 'Content' } },
            { p: { text: 'Main content here' } }
          ]
        }} //section
      ]
    }}, //main.content
    { footer: {
      className: 'app-footer',
      text: '© 2024 App Name'
    }} //footer.app-footer
  ]
}} //div.layout

Reactivity Patterns

Basic Reactive Text

{ span: {
  text: () => getState('counter.value', 0)
}}

Conditional Reactive Content

{ div: {
  text: () => {
    const user = getState('user.current', null);
    return user ? `Welcome, ${user.name}` : 'Please login';
  }
}}

Reactive Children

{ ul: {
  children: () => {
    const items = getState('list.items', []);
    return items.map(item => ({ li: { text: item.name, key: item.id } }));
  }
}}

Reactive Styling

{ div: {
  className: 'status-indicator',
  style: () => ({
    backgroundColor: getState('system.status', 'unknown') === 'ok' ? 'green' : 'red',
    opacity: getState('ui.visible', true) ? 1 : 0.5
  })
}}

Subscription Control

Default Subscription (Reactive)

{ div: {
  text: () => getState('data.message', 'Loading...') //subscribes to changes
}}

Skip Subscription (Read-Once)

{ div: {
  text: () => {
    const config = getState('app.config', {}, false); //no subscription
    const liveData = getState('live.data', ''); //subscribes to changes
    return `${config.prefix}: ${liveData}`;
  }
}}

Form Conventions

Form Structure

{ form: {
  className: 'user-form',
  onsubmit: (e) => handleSubmit(e),
  children: [
    { fieldset: {
      children: [
        { legend: { text: 'User Information' } },
        { div: {
          className: 'form-row',
          children: [
            { label: { htmlFor: 'username', text: 'Username:' } },
            { input: {
              id: 'username', type: 'text', name: 'username',
              value: () => getState('form.username', ''),
              oninput: (e) => setState('form.username', e.target.value)
            }}
          ]
        }}, //div.form-row
        { div: {
          className: 'form-actions',
          children: [
            { button: { type: 'submit', text: 'Save' } },
            { button: { type: 'reset', text: 'Reset' } }
          ]
        }} //div.form-actions
      ]
    }} //fieldset
  ]
}} //form.user-form

Table Conventions

Table Structure

{ table: {
  className: 'data-grid',
  children: [
    { thead: {
      children: [
        { tr: {
          children: [
            { th: { text: 'Name' } },
            { th: { text: 'Status' } },
            { th: { text: 'Actions' } }
          ]
        }}
      ]
    }}, //thead
    { tbody: {
      children: () => {
        const users = getState('users.list', []);
        return users.map(user => ({ tr: {
          key: user.id,
          children: [
            { td: { text: user.name } },
            { td: {
              className: () => `status ${getState(`users.${user.id}.status`, 'inactive')}`,
              text: () => getState(`users.${user.id}.status`, 'inactive')
            }},
            { td: {
              children: [
                { button: { text: 'Edit', onclick: () => editUser(user.id) } }
              ]
            }}
          ]
        }}));
      }
    }} //tbody
  ]
}} //table.data-grid

Component Definition Pattern

Function Component (Correct)

const UserCard = (props, context) => {
  const { getState, setState } = context;
  
  return {
    div: {
      className: 'user-card',
      children: [
        { img: {
          src: () => getState(`users.${props.userId}.avatar`, '/default-avatar.png'),
          alt: 'User Avatar'
        }},
        { div: {
          className: 'user-info',
          children: [
            { h3: {
              text: () => getState(`users.${props.userId}.name`, 'Unknown')
            }},
            { p: {
              text: () => getState(`users.${props.userId}.email`, '')
            }}
          ]
        }} //div.user-info
      ]
    } //div.user-card
  }; //return
};

// Register separately
juris.registerComponent('UserCard', UserCard);

Event Handling Patterns

Simple Events

{ button: {
  text: 'Click Me',
  onclick: () => handleClick()
}}

Events with State Updates

{ input: {
  type: 'text', placeholder: 'Search...',
  value: () => getState('search.query', ''),
  oninput: (e) => setState('search.query', e.target.value),
  onkeydown: (e) => {
    if (e.key === 'Enter') {
      performSearch(getState('search.query', ''));
    }
  }
}}

Common Patterns Summary

Inline (Same Line)

  • Static short properties: className: 'btn'
  • Static attributes: type: 'text', required: true
  • Simple static objects: style: { color: 'red' }

New Line

  • Reactive functions: text: () => getState('path')
  • Complex logic: Multi-line functions
  • Event handlers: onclick: () => handler()

End Labels

  • Complex elements: }} //elementType.className
  • Nested structures: }} //section.main-content
  • Forms/tables: }} //form.user-form, }} //table.data-grid

Array Items

  • Always: { elementType: { props } }
  • Never: elementType: { props } (missing braces)

This formatting ensures readable, maintainable Juris components that follow the framework's conventions.