Skip to content

Juris WebComponent Factory

jurisauthor edited this page Aug 26, 2025 · 1 revision

A comprehensive web component system that implements the complete web component standards while providing deep integration with the Juris reactive framework.

Features

Core Web Component Standards

  • Custom element registration with full lifecycle support
  • Shadow DOM with configurable modes and focus delegation
  • Observed attributes with automatic type coercion
  • CSS encapsulation with :host selectors and custom properties

Reactive Props System

  • Function-based reactive props that auto-update on state changes
  • Mixed static and reactive prop support
  • Automatic dependency tracking and subscription management
  • Maintains Juris reactivity across component boundaries

Form Integration

  • Native form participation with formAssociated = true
  • Built-in validation with setValidity() and form internals
  • Form lifecycle callbacks and state restoration
  • Seamless integration with HTML form validation

Accessibility (A11y)

  • ARIA attribute observation and forwarding
  • Screen reader announcements with live regions
  • Keyboard navigation and focus management
  • Automatic accessibility patterns and roles

Advanced Slot System

  • Named and unnamed slot support with change observation
  • Content projection with automatic slot information
  • Slot-based re-rendering triggers

Performance & Standards

  • Global event delegation for optimal performance
  • Memory leak prevention with automatic cleanup
  • Complete standards compliance beyond basic web components
  • Async rendering support with error boundaries

Installation

Include the WebComponent factory after the main Juris library:

<script src="juris.js"></script>
<script src="juris-webcomponent.js"></script>

Quick Start

1. Initialize Juris with WebComponent Support

const juris = new Juris({
    features: {
        webComponentFactory: WebComponentFactory
    }
});

2. Define Components

const MyButton = (props, { getState, setState }) => {
    return {
        // Observed HTML attributes
        attributes: ['type', 'disabled'],
        
        // Initial component state
        initialState: {
            clickCount: 0
        },
        
        // Web component options
        options: {
            shadowMode: 'open',
            formAssociated: true,
            delegatesFocus: true,
            accessibility: {
                defaultRole: 'button',
                focusable: true
            }
        },
        
        // Exposed API methods
        api: {
            click() {
                this.click();
            },
            focus() {
                this.focus();
            },
            getClickCount() {
                return getState('clickCount', 0);
            }
        },
        
        // Form integration
        form: {
            getValue: () => getState('value', ''),
            validate: () => ({
                flags: { valueMissing: !getState('value', '') },
                message: 'Value is required'
            })
        },
        
        // Accessibility configuration
        accessibility: {
            liveRegion: true,
            keyHandlers: {
                'Enter': () => this.click(),
                'Space': () => this.click()
            }
        },
        
        // Lifecycle hooks
        hooks: {
            onMount: (context) => {
                context.component.announceToScreenReader('Button ready');
            },
            onSlotChange: (slotInfo, context) => {
                console.log('Slot content changed:', slotInfo);
            }
        },
        
        // Render function
        render: () => ({
            button: {
                type: props.type || 'button',
                disabled: () => props.disabled,
                onclick: () => {
                    const count = getState('clickCount', 0);
                    setState('clickCount', count + 1);
                },
                children: [
                    {slot: { name: 'icon' }},
                    'Click me ',
                    () => `(${getState('clickCount', 0)})`
                ]
            }
        })
    };
};

3. Register Components

// Auto-registration from config
juris.webComponentFactory.initializeFromConfig({
    'my-button': MyButton
}, () => juris.createContext());

// Manual registration
juris.webComponentFactory.create('my-button', MyButton, {
    shadowMode: 'open',
    formAssociated: true
});

4. Use in Layout with Reactive Props

juris.layout = {
    div: {
        children: [
            {
                'my-button': {
                    type: 'submit',
                    disabled: () => juris.getState('formInvalid', false),
                    label: () => `Submit (${juris.getState('pendingItems', 0)} pending)`
                }
            }
        ]
    }
};

juris.render();

Component Configuration

Complete Configuration Object

const ComponentDefinition = (props, context) => {
    return {
        // Observed attributes
        attributes: ['value', 'disabled', 'required'],
        
        // Initial state
        initialState: {
            internalValue: '',
            valid: true
        },
        
        // Web component options
        options: {
            shadowMode: 'open',           // 'open' | 'closed' | 'none'
            delegatesFocus: true,         // Focus delegation
            slotAssignment: 'named',      // 'named' | 'manual'
            formAssociated: true,         // Enable form participation
            enhanceMode: false            // Use light DOM instead of shadow
        },
        
        // API methods exposed on element
        api: {
            getValue() { return getState('value', ''); },
            setValue(value) { setState('value', value); },
            validate() { return this.checkValidity(); }
        },
        
        // Form integration
        form: {
            getValue: (context) => getState('value', ''),
            validate: (context) => ({
                flags: { 
                    valueMissing: !getState('value', ''),
                    patternMismatch: false 
                },
                message: 'Please fill out this field'
            }),
            reset: (context) => setState('value', ''),
            restore: (state, mode, context) => setState('value', state)
        },
        
        // Accessibility configuration
        accessibility: {
            defaultRole: 'textbox',
            liveRegion: true,
            focusable: true,
            keyHandlers: {
                'Enter': (e, context) => this.form.requestSubmit(),
                'Escape': (e, context) => this.blur()
            }
        },
        
        // Event delegation
        events: {
            'focus': (e, context) => setState('focused', true),
            'blur': (e, context) => setState('focused', false)
        },
        
        // Lifecycle hooks
        hooks: {
            onConnect: (context) => console.log('Connected'),
            onMount: (context) => console.log('Mounted'),
            onUnmount: (context) => console.log('Unmounted'),
            onAdopted: (context) => console.log('Adopted'),
            onAttributeChange: (name, oldVal, newVal, context) => {},
            onSlotChange: (slotInfo, context) => {},
            onFormAssociated: (form, context) => {},
            onFormReset: (context) => {},
            onAriaChange: (attr, value, ariaState, context) => {}
        },
        
        // Render function
        render: () => ({
            div: {
                class: () => `input-wrapper ${getState('focused', false) ? 'focused' : ''}`,
                children: [
                    {input: {
                        type: 'text',
                        value: () => getState('value', ''),
                        oninput: (e) => setState('value', e.target.value)
                    }},
                    {slot: { name: 'helper-text' }}
                ]
            }
        })
    };
};

Reactive Props System

Static Props

// In layout
'my-component': {
    title: 'Hello World',        // Static string
    count: 42,                   // Static number
    config: { theme: 'dark' }    // Static object
}

// In component
render: () => ({
    div: {
        text: props.title,       // 'Hello World'
        class: props.config.theme // 'dark'
    }
})

Reactive Props

// In layout
'my-component': {
    title: () => juris.getState('pageTitle', 'Default'),
    count: () => juris.getState('itemCount', 0),
    config: () => juris.getState('appConfig', {})
}

// In component - props are reactive functions
render: () => ({
    div: {
        text: props.title,       // Function that updates automatically
        data-count: props.count, // Updates when state changes
        class: () => props.config().theme
    }
})

Form Integration

Basic Form Component

const FormInput = (props, { getState, setState, component }) => {
    return {
        options: { formAssociated: true },
        
        form: {
            getValue: () => getState('value', ''),
            
            validate: () => {
                const value = getState('value', '');
                const required = props.required && props.required();
                
                return {
                    flags: { 
                        valueMissing: required && !value,
                        tooShort: value.length < 3
                    },
                    message: !value ? 'This field is required' : 
                            value.length < 3 ? 'Minimum 3 characters' : ''
                };
            },
            
            reset: () => setState('value', ''),
            
            restore: (state) => setState('value', state)
        },
        
        render: () => ({
            input: {
                type: 'text',
                value: () => getState('value', ''),
                oninput: (e) => {
                    setState('value', e.target.value);
                    component.setFormValue(e.target.value);
                }
            }
        })
    };
};

Accessibility Features

Screen Reader Support

const AccessibleCounter = (props, { getState, setState, component }) => {
    return {
        accessibility: {
            defaultRole: 'button',
            liveRegion: true,
            keyHandlers: {
                'Enter': () => increment(),
                'Space': () => increment(),
                'ArrowUp': () => increment(),
                'ArrowDown': () => decrement()
            }
        },
        
        api: {
            increment() {
                const newCount = getState('count', 0) + 1;
                setState('count', newCount);
                component.announceToScreenReader(`Count is now ${newCount}`);
            }
        },
        
        render: () => ({
            div: {
                'aria-label': () => `Counter: ${getState('count', 0)}`,
                tabindex: '0',
                text: () => getState('count', 0)
            }
        })
    };
};

Advanced Slot Usage

Component with Named Slots

const CardComponent = (props, { component }) => {
    return {
        hooks: {
            onSlotChange: (slots) => {
                console.log('Available slots:', Object.keys(slots));
                
                // Check if specific slots have content
                if (slots.header?.assignedElements.length > 0) {
                    console.log('Header slot has content');
                }
            }
        },
        
        render: () => ({
            div: {
                class: 'card',
                children: [
                    {header: {
                        class: 'card-header',
                        children: [{slot: { name: 'header' }}]
                    }},
                    {main: {
                        class: 'card-content',
                        children: [{slot: {}}] // Default slot
                    }},
                    {footer: {
                        class: 'card-footer',
                        children: [{slot: { name: 'actions' }}]
                    }}
                ]
            }
        })
    };
};

Usage with Slot Content

<my-card>
    <h2 slot="header">Card Title</h2>
    <p>This goes in the default slot</p>
    <button slot="actions">Action Button</button>
</my-card>

API Integration

Using Component APIs

// Get component reference
const button = document.querySelector('my-button');

// Call API methods directly
button.increment();
button.reset();
const stats = button.getStats();

// Check validation state
const isValid = button.checkValidity();
const validation = button.getValidationState();

// Form methods
button.setCustomValidity('Custom error message');
button.reportValidity();

Performance Considerations

Optimization Tips

  1. Use static props for unchanging data
// Good - static
title: 'Fixed Title'

// Avoid - reactive for static data
title: () => 'Fixed Title'
  1. Batch state updates
juris.executeBatch(() => {
    setState('prop1', value1);
    setState('prop2', value2);
    setState('prop3', value3);
});
  1. Use form internals for form controls
form: {
    getValue: () => getState('value', ''),
    validate: () => ({ flags: {}, message: '' })
}
  1. Leverage slot observation judiciously
hooks: {
    onSlotChange: (slots) => {
        // Only re-render if slot change affects component
        if (slots.critical?.assignedElements.length > 0) {
            this.render();
        }
    }
}

Browser Support

  • Modern browsers: Full support (Chrome 67+, Firefox 63+, Safari 13+)
  • Form association: Chrome 77+, Firefox 93+, Safari 16.4+
  • Slot assignment: Chrome 86+, Firefox 92+, Safari 16.4+

Comparison with Other Libraries

Feature Juris WebComponents Lit Stencil
Reactive Props Function-based Property-based Property-based
Form Participation Built-in Manual Manual
Accessibility Automatic Manual Manual
State Integration Deep integration External External
Slot Observation Automatic Manual Manual
Event Delegation Global Per-component Per-component
Bundle Size Framework-dependent ~5KB ~1KB

Contributing

The WebComponent Factory is part of the Juris framework. Contributions should maintain backward compatibility and follow web component standards.

License

MIT License - see Juris framework license for details.

Clone this wiki locally