Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

AltStyle によって変換されたページ (->オリジナル) /