-
Notifications
You must be signed in to change notification settings - Fork 8
Object VDOM Conventions
jurisauthor edited this page Aug 17, 2025
·
2 revisions
Focus: Object VDOM structure conventions and reactivity patterns only
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.
- 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
return { div: { class: '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
- Static & Short Properties: Same line as element
- Reactive Properties: New line with function syntax
-
Element Arrays:
{ element: { props }}format -
End Labels: Add
//elementType.classNameor//elementTypefor identification
{ div: { class: 'container', id: 'main', tabIndex: 0 } } { button: { type: 'submit', disabled: true, text: 'Save' } } { input: { type: 'email', required: true, placeholder: 'Enter email' } }
{ div: { text: () => getState('user.name', 'Anonymous'), class: () => `status ${getState('user.online', false) ? 'online' : 'offline'}`, style: () => ({ color: getState('theme.color', 'black') }) }}
{ button: { type: 'button', class: 'action-btn', //static inline disabled: () => getState('form.saving', false), //reactive new line text: () => getState('form.saving', false) ? 'Saving...' : 'Save', //reactive new line onclick: () => handleSave() }}
{ div: { class: 'container', children: [ { h1: { text: 'Title' } }, { p: { text: 'Description' } }, { button: { text: 'Action' } } ] }}
{ div: { class: 'layout', children: [ { header: { class: 'app-header', children: [ { h1: { text: 'App Name' } }, { nav: { children: [ { a: { href: '/', text: 'Home' } }, { a: { href: '/about', text: 'About' } } ] }} //nav ] }}, //header.app-header { main: { class: 'content', children: [ { section: { children: [ { h2: { text: 'Content' } }, { p: { text: 'Main content here' } } ] }} //section ] }}, //main.content { footer: { class: 'app-footer', text: '© 2024 App Name' }} //footer.app-footer ] }} //div.layout
{ span: { text: () => getState('counter.value', 0) }}
{ div: { text: () => { const user = getState('user.current', null); return user ? `Welcome, ${user.name}` : 'Please login'; } }}
{ ul: { children: () => { const items = getState('list.items', []); return items.map(item => ({ li: { text: item.name, key: item.id } })); } }}
{ div: { class: 'status-indicator', style: () => ({ backgroundColor: getState('system.status', 'unknown') === 'ok' ? 'green' : 'red', opacity: getState('ui.visible', true) ? 1 : 0.5 }) }}
{ div: { text: () => getState('data.message', 'Loading...') //subscribes to changes }}
{ div: { text: () => { const config = getState('app.config', {}, false); //no subscription const liveData = getState('live.data', ''); //subscribes to changes return `${config.prefix}: ${liveData}`; } }}
{ form: { class: 'user-form', onsubmit: (e) => handleSubmit(e), children: [ { fieldset: { children: [ { legend: { text: 'User Information' } }, { div: { class: '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: { class: 'form-actions', children: [ { button: { type: 'submit', text: 'Save' } }, { button: { type: 'reset', text: 'Reset' } } ] }} //div.form-actions ] }} //fieldset ] }} //form.user-form
{ table: { class: '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: { class: () => `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
const UserCard = (props, context) => { const { getState, setState } = context; return { div: { class: 'user-card', children: [ { img: { src: () => getState(`users.${props.userId}.avatar`, '/default-avatar.png'), alt: 'User Avatar' }}, { div: { class: '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);
{ button: { text: 'Click Me', onclick: () => handleClick() }}
{ 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', '')); } } }}
- Static short properties:
class: 'btn' - Static attributes:
type: 'text', required: true - Simple static objects:
style: { color: 'red' }
- Reactive functions:
text: () => getState('path') - Complex logic: Multi-line functions
- Event handlers:
onclick: () => handler()
- Complex elements:
}} //elementType.className - Nested structures:
}} //section.main-content - Forms/tables:
}} //form.user-form,}} //table.data-grid
- Always:
{ elementType: { props } } - Never:
elementType: { props }(missing braces)
This formatting ensures readable, maintainable Juris components that follow the framework's conventions.