juris-test
Version:
JavaScript Unified Reactive Interface Solution - Browser-optimized version for script tags and CDN usage
399 lines (338 loc) • 18.5 kB
JavaScript
// juris-webcomponent.js - Standalone WebComponent Factory Feature
if (typeof WebComponentFactory === 'undefined') {
class WebComponentFactory {
constructor(jurisInstance) {
this.juris = jurisInstance;
}
create(name, componentDefinition, options = {}) {
console.info(log.i('Creating WebComponent', { name, hasOptions: Object.keys(options).length > 0 }, 'framework'));
if (!name.includes('-')) {
throw new Error(`WebComponent name "${name}" must contain a hyphen (-)`);
}
if (customElements.get(name)) {
console.warn(log.w('WebComponent already registered', { name }, 'framework'));
return customElements.get(name);
}
const WebComponentClass = this._createWebComponentClass(name, componentDefinition, options);
customElements.define(name, WebComponentClass);
console.info(log.i('WebComponent registered', { name, className: WebComponentClass.name }, 'framework'));
return WebComponentClass;
}
createMultiple(components, globalOptions = {}) {
const registeredComponents = {};
Object.entries(components).forEach(([name, definition]) => {
const options = definition.options ?
{ ...globalOptions, ...definition.options } :
globalOptions;
const componentFn = definition.component || definition.render || definition;
registeredComponents[name] = this.create(name, componentFn, options);
});
return registeredComponents;
}
_createWebComponentClass(name, componentDefinition, options) {
const jurisInstance = this.juris;
const {
shadowMode = 'open',
attributes = [],
styles = '',
enhanceMode = false,
autoConnect = true,
stateNamespace = null,
contextProvider = null
} = options;
return class JurisWebComponent extends HTMLElement {
static get observedAttributes() {
return attributes;
}
constructor() {
super();
this.componentName = name;
this.componentId = `${name}-${Math.random().toString(36).substr(2, 9)}`;
this.isJurisComponent = true;
this._mounted = false;
this._unsubscribes = [];
if (typeof componentDefinition === 'function') {
this.componentFn = componentDefinition;
} else if (typeof componentDefinition === 'object') {
this.componentConfig = componentDefinition;
this.componentFn = componentDefinition.render || componentDefinition.component;
}
console.debug(log.d('WebComponent instance created', { name, componentId: this.componentId }, 'framework'));
}
connectedCallback() {
if (!autoConnect) return;
console.debug(log.d('WebComponent connecting', { name, componentId: this.componentId }, 'framework'));
this._setupShadowDOM();
this._setupJurisIntegration();
this._setupAttributes();
this._setupStyles();
if (this.componentConfig?.hooks?.onConnect) {
this.componentConfig.hooks.onConnect.call(this, this.jurisContext);
}
this.render();
this._mounted = true;
if (this.componentConfig?.hooks?.onMount) {
requestAnimationFrame(() => {
this.componentConfig.hooks.onMount.call(this, this.jurisContext);
});
}
}
disconnectedCallback() {
console.debug(log.d('WebComponent disconnecting', { name, componentId: this.componentId }, 'framework'));
this._mounted = false;
this._unsubscribes.forEach(unsubscribe => {
try {
unsubscribe();
} catch (error) {
console.warn(log.w('Error during subscription cleanup:', error), 'framework');
}
});
this._unsubscribes = [];
if (this.componentConfig?.hooks?.onUnmount) {
this.componentConfig.hooks.onUnmount.call(this, this.jurisContext);
}
if (this.stateKey && options.cleanupState !== false) {
jurisInstance.stateManager.setState(this.stateKey, undefined);
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue && this._mounted) {
console.debug(log.d('Attribute changed', { name, oldValue, newValue }, 'framework'));
if (this.stateKey) {
const currentState = jurisInstance.getState(this.stateKey, {});
jurisInstance.setState(this.stateKey, {
...currentState,
[name]: this._parseAttributeValue(newValue)
});
}
if (this.componentConfig?.hooks?.onAttributeChange) {
this.componentConfig.hooks.onAttributeChange.call(this, name, oldValue, newValue, this.jurisContext);
}
if (options.rerenderOnAttributeChange !== false) {
this.render();
}
}
}
_setupShadowDOM() {
if (!enhanceMode) {
this.attachShadow({ mode: shadowMode });
this.renderRoot = this.shadowRoot;
} else {
this.renderRoot = this;
}
}
_setupJurisIntegration() {
this.stateKey = stateNamespace || `webcomponents.${name.replace(/-/g, '_')}.${this.componentId}`;
const initialState = this._getInitialState();
jurisInstance.setState(this.stateKey, initialState);
this.jurisContext = this._createJurisContext();
const unsubscribe = jurisInstance.subscribe(this.stateKey, () => {
if (this._mounted && options.autoRerender !== false) {
console.debug(log.d('Auto re-rendering due to state change', { componentId: this.componentId }, 'framework'));
this.render();
}
});
this._unsubscribes.push(unsubscribe);
}
_createJurisContext() {
const baseContext = contextProvider ?
contextProvider.call(this, jurisInstance.createContext(this)) :
jurisInstance.createContext(this);
return {
...baseContext,
component: {
name: this.componentName,
id: this.componentId,
element: this,
renderRoot: this.renderRoot,
shadowRoot: this.shadowRoot,
getState: (key, defaultValue) => {
const fullKey = key ? `${this.stateKey}.${key}` : this.stateKey;
return jurisInstance.getState(fullKey, defaultValue);
},
setState: (key, value) => {
if (typeof key === 'object') {
const currentState = jurisInstance.getState(this.stateKey, {});
jurisInstance.setState(this.stateKey, { ...currentState, ...key });
} else {
const fullKey = key ? `${this.stateKey}.${key}` : this.stateKey;
jurisInstance.setState(fullKey, value);
}
},
updateState: (updates) => {
const currentState = jurisInstance.getState(this.stateKey, {});
jurisInstance.setState(this.stateKey, { ...currentState, ...updates });
},
getAttribute: (name, defaultValue = null) => {
return this.getAttribute(name) || defaultValue;
},
setAttribute: (name, value) => {
this.setAttribute(name, value);
},
emit: (eventName, detail = {}, options = {}) => {
const event = new CustomEvent(eventName, {
detail,
bubbles: true,
composed: true,
...options
});
this.dispatchEvent(event);
return event;
},
getSlot: (name = '') => {
return name ?
this.querySelector(`[slot="${name}"]`) :
this.querySelector(':not([slot])');
},
getAllSlots: () => {
const slots = {};
this.querySelectorAll('[slot]').forEach(el => {
const slotName = el.getAttribute('slot');
if (!slots[slotName]) slots[slotName] = [];
slots[slotName].push(el);
});
return slots;
}
}
};
}
_getInitialState() {
let initialState = {};
if (this.componentConfig?.initialState) {
if (typeof this.componentConfig.initialState === 'function') {
initialState = this.componentConfig.initialState.call(this);
} else {
initialState = { ...this.componentConfig.initialState };
}
}
if (this.componentFn?.getInitialState) {
initialState = { ...initialState, ...this.componentFn.getInitialState.call(this) };
}
attributes.forEach(attr => {
if (this.hasAttribute(attr)) {
initialState[attr] = this._parseAttributeValue(this.getAttribute(attr));
}
});
return initialState;
}
_setupAttributes() {
attributes.forEach(attr => {
if (this.hasAttribute(attr)) {
const value = this._parseAttributeValue(this.getAttribute(attr));
this.jurisContext.component.setState(attr, value);
}
});
}
_setupStyles() {
if (styles && this.shadowRoot) {
const styleElement = document.createElement('style');
styleElement.textContent = styles;
this.shadowRoot.appendChild(styleElement);
}
}
_parseAttributeValue(value) {
if (value === null || value === undefined) return value;
if (value === 'true') return true;
if (value === 'false') return false;
if (value === '') return true; // Boolean attribute
if (!isNaN(value) && !isNaN(parseFloat(value))) return parseFloat(value);
try {
return JSON.parse(value);
} catch {
return value;
}
}
render() {
try {
console.debug(log.d('Rendering WebComponent', { componentId: this.componentId }, 'framework'));
let vdom;
if (this.componentFn) {
vdom = this.componentFn.call(this, this._getProps(), this.jurisContext);
} else if (this.componentConfig?.template) {
vdom = this.componentConfig.template.call(this, this._getProps(), this.jurisContext);
} else {
console.warn(log.w('No render method found for WebComponent', { name }, 'framework'));
return;
}
if (vdom?.then) {
this._handleAsyncRender(vdom);
return;
}
if (vdom) {
const element = jurisInstance.objectToHtml(vdom);
this.renderRoot.innerHTML = '';
if (this.shadowRoot && styles) {
const styleElement = document.createElement('style');
styleElement.textContent = styles;
this.renderRoot.appendChild(styleElement);
}
this.renderRoot.appendChild(element);
}
} catch (error) {
console.error(log.e('WebComponent render error', {
name,
componentId: this.componentId,
error: error.message
}, 'framework'));
this._renderError(error);
}
}
_handleAsyncRender(vdomPromise) {
this.renderRoot.innerHTML = '<div class="juris-loading">Loading...</div>';
jurisInstance.promisify(vdomPromise)
.then(vdom => {
if (this._mounted) {
const element = jurisInstance.objectToHtml(vdom);
this.renderRoot.innerHTML = '';
this.renderRoot.appendChild(element);
}
})
.catch(error => {
console.error(log.e('Async render error', { error: error.message }, 'framework'));
this._renderError(error);
});
}
_renderError(error) {
const errorElement = document.createElement('div');
errorElement.style.cssText = 'color: red; padding: 10px; border: 1px solid red; background: #fee;';
errorElement.textContent = `Component Error: ${error.message}`;
this.renderRoot.innerHTML = '';
this.renderRoot.appendChild(errorElement);
}
_getProps() {
const props = {};
attributes.forEach(attr => {
if (this.hasAttribute(attr)) {
props[attr] = this._parseAttributeValue(this.getAttribute(attr));
}
});
const state = jurisInstance.getState(this.stateKey, {});
Object.assign(props, state);
return props;
}
forceRender() {
this.render();
}
getJurisContext() {
return this.jurisContext;
}
getComponentState() {
return jurisInstance.getState(this.stateKey, {});
}
updateComponentState(updates) {
this.jurisContext.component.updateState(updates);
}
};
}
}
// Register feature automatically
if (typeof window !== 'undefined') {
window.WebComponentFactory = WebComponentFactory;
Object.freeze(window.WebComponentFactory);
Object.freeze(window.WebComponentFactory.prototype);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports.WebComponentFactory = WebComponentFactory;
module.exports.default = WebComponentFactory;
}
}