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
- Reactivity works when getState is called from intended functional attributes and children
- Use compress object structure and add labels into the end brackets for nested divs, tables, select, groups and forms
- Use getState third attribute with false value to skip subscription
- Components will not re-render until their parent triggers re-render
- All props and attributes can handle async/sync natively
- Use service injection AMAP
- 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:
- Static & Short Properties: Same line as element
- Reactive Properties: New line with function syntax
- Element Arrays:
{ element: { props }}
format - 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.