Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions priv/templates/arizona.svelte.template
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
{template, "arizona.svelte/assets/js/arizona-svelte-registry.js", "{{name}}/assets/js/arizona-svelte-registry.js"}.
{template, "arizona.svelte/assets/svelte/components/Counter.svelte", "{{name}}/assets/svelte/components/Counter.svelte"}.
{template, "arizona.svelte/assets/svelte/components/HelloWorld.svelte", "{{name}}/assets/svelte/components/HelloWorld.svelte"}.
{template, "arizona.svelte/assets/svelte/components/LifecycleDemo.svelte", "{{name}}/assets/svelte/components/LifecycleDemo.svelte"}.
{template, "arizona.svelte/assets/tailwind.config.js", "{{name}}/assets/tailwind.config.js"}.
{template, "arizona.svelte/assets/vite.config.js", "{{name}}/assets/vite.config.js"}.

Expand Down
309 changes: 305 additions & 4 deletions priv/templates/arizona.svelte/assets/js/arizona-svelte-lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@
import { mount, unmount } from 'svelte';

class ArizonaSvelteLifecycle {
constructor(registry) {
constructor(registry, options = {}) {
if (!registry) {
throw new Error('ArizonaSvelteLifecycle requires a registry instance');
}

this.registry = registry;
this.mountedComponents = new Map(); // target -> component instance
this.observers = new Set(); // Set of active observers
this.isMonitoring = false;
this.options = {
autoMount: options.autoMount !== false, // Default true
autoUnmount: options.autoUnmount !== false, // Default true
observeSubtree: options.observeSubtree !== false, // Default true
debounceMs: options.debounceMs || 100, // Debounce DOM changes
...options
};
this.debounceTimer = null;
}

/**
Expand All @@ -38,15 +48,22 @@ class ArizonaSvelteLifecycle {
const instance = mount(ComponentClass, { target, props });
this.mountedComponents.set(target, instance);
mountedCount++;
console.log(`[Arizona Svelte] ✅ Mounted '${componentName}' component`, {
target: target.id || target.className || 'unnamed',
props,
totalMounted: this.mountedComponents.size
});
} catch (error) {
console.error(`[Arizona Svelte] Failed to mount component '${componentName}':`, error);
console.error(`[Arizona Svelte] Failed to mount component '${componentName}':`, error);
}
} else {
console.warn(`[Arizona Svelte] Component '${componentName}' not found in registry`);
}
});

console.log(`[Arizona Svelte] Mounted ${mountedCount} components`);
if (mountedCount > 0) {
console.log(`[Arizona Svelte] Mounted ${mountedCount} components`);
}
return mountedCount;
}

Expand All @@ -60,11 +77,16 @@ class ArizonaSvelteLifecycle {

if (instance) {
try {
const componentName = target.dataset.svelteComponent || 'unknown';
unmount(instance);
this.mountedComponents.delete(target);
console.log(`[Arizona Svelte] 🗑️ Unmounted '${componentName}' component`, {
target: target.id || target.className || 'unnamed',
totalMounted: this.mountedComponents.size
});
return true;
} catch (error) {
console.error(`[Arizona Svelte] Failed to unmount component:`, error);
console.error(`[Arizona Svelte] Failed to unmount component:`, error);
return false;
}
}
Expand Down Expand Up @@ -123,6 +145,285 @@ class ArizonaSvelteLifecycle {
isComponentMounted(target) {
return this.mountedComponents.has(target);
}

/**
* Start automatic monitoring for component lifecycle
* @returns {void}
*/
startMonitoring() {
if (this.isMonitoring) {
console.warn('[Arizona Svelte] Monitoring already started');
return;
}

this.isMonitoring = true;
console.log('[Arizona Svelte] Starting automatic component monitoring');

// Initial mount
if (this.options.autoMount) {
this.mountComponents();
}

// Set up DOM mutation observer
this.setupDOMObserver();

// Set up Arizona WebSocket listener
this.setupArizonaListener();

// Set up page visibility listener
this.setupVisibilityListener();

// Set up cleanup on page unload
this.setupUnloadListener();
}

/**
* Stop automatic monitoring
* @returns {void}
*/
stopMonitoring() {
if (!this.isMonitoring) {
return;
}

this.isMonitoring = false;
console.log('[Arizona Svelte] Stopping automatic component monitoring');

// Clean up all observers
this.observers.forEach(observer => {
if (observer.disconnect) {
observer.disconnect();
} else if (typeof observer === 'function') {
observer(); // Cleanup function
}
});
this.observers.clear();

// Clear debounce timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
}

/**
* Setup DOM mutation observer to detect component additions/removals
* @private
*/
setupDOMObserver() {
const observer = new MutationObserver((mutations) => {
this.debouncedHandleMutations(mutations);
});

observer.observe(document.body, {
childList: true,
subtree: this.options.observeSubtree,
attributes: true,
attributeFilter: ['data-svelte-component', 'data-svelte-props']
});

this.observers.add(observer);
}

/**
* Setup Arizona WebSocket event listener for patches
* @private
*/
setupArizonaListener() {
const handleArizonaEvent = (event) => {
const { type, data } = event.detail;

if (type === 'html_patch') {
// When Arizona applies HTML patches, we need to check for new components
this.debouncedScanAndMount();
}
};

document.addEventListener('arizonaEvent', handleArizonaEvent);

// Return cleanup function
const cleanup = () => {
document.removeEventListener('arizonaEvent', handleArizonaEvent);
};

this.observers.add(cleanup);
}

/**
* Setup page visibility listener to pause/resume components
* @private
*/
setupVisibilityListener() {
const handleVisibilityChange = () => {
if (document.hidden) {
console.log('[Arizona Svelte] Page hidden - components may pause updates');
} else {
console.log('[Arizona Svelte] Page visible - checking for component updates');
this.debouncedScanAndMount();
}
};

document.addEventListener('visibilitychange', handleVisibilityChange);

const cleanup = () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};

this.observers.add(cleanup);
}

/**
* Setup page unload listener for cleanup
* @private
*/
setupUnloadListener() {
const handleUnload = () => {
console.log('[Arizona Svelte] Page unloading - cleaning up components');
if (this.options.autoUnmount) {
this.unmountAllComponents();
}
this.stopMonitoring();
};

window.addEventListener('beforeunload', handleUnload);
window.addEventListener('unload', handleUnload);

const cleanup = () => {
window.removeEventListener('beforeunload', handleUnload);
window.removeEventListener('unload', handleUnload);
};

this.observers.add(cleanup);
}

/**
* Debounced mutation handler to avoid excessive re-scanning
* @private
*/
debouncedHandleMutations(mutations) {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}

this.debounceTimer = setTimeout(() => {
this.handleMutations(mutations);
}, this.options.debounceMs);
}

/**
* Debounced scan and mount to avoid excessive operations
* @private
*/
debouncedScanAndMount() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}

this.debounceTimer = setTimeout(() => {
this.scanAndMount();
}, this.options.debounceMs);
}

/**
* Handle DOM mutations and update components accordingly
* @private
*/
handleMutations(mutations) {
let shouldScan = false;
const removedNodes = new Set();

mutations.forEach(mutation => {
// Handle removed nodes
if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
removedNodes.add(node);
// Check if removed node or its children had mounted components
this.unmountRemovedComponents(node);
}
});
}

// Handle added nodes or attribute changes
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
shouldScan = true;
} else if (mutation.type === 'attributes' &&
(mutation.attributeName === 'data-svelte-component' ||
mutation.attributeName === 'data-svelte-props')) {
shouldScan = true;
}
});

if (shouldScan && this.options.autoMount) {
this.scanAndMount();
}
}

/**
* Scan for new components and mount them
* @private
*/
async scanAndMount() {
try {
const mounted = await this.mountComponents();
if (mounted > 0) {
console.log(`[Arizona Svelte] 🔄 Auto-mounted ${mounted} new components`);
}
} catch (error) {
console.error('[Arizona Svelte] Error during auto-mount:', error);
}
}

/**
* Unmount components that were removed from DOM
* @private
*/
unmountRemovedComponents(removedNode) {
if (!this.options.autoUnmount) {
return;
}

// Check if the removed node itself was a component target
if (this.mountedComponents.has(removedNode)) {
console.log('[Arizona Svelte] Auto-unmounting removed component');
this.unmountComponent(removedNode);
}

// Check children of removed node
if (removedNode.querySelectorAll) {
const childTargets = removedNode.querySelectorAll('[data-svelte-component]');
childTargets.forEach(target => {
if (this.mountedComponents.has(target)) {
console.log('[Arizona Svelte] Auto-unmounting removed child component');
this.unmountComponent(target);
}
});
}
}

/**
* Get monitoring status
* @returns {boolean}
*/
isMonitoringActive() {
return this.isMonitoring;
}

/**
* Get current monitoring options
* @returns {Object}
*/
getMonitoringOptions() {
return { ...this.options };
}

/**
* Update monitoring options
* @param {Object} newOptions - New options to merge
*/
updateMonitoringOptions(newOptions) {
this.options = { ...this.options, ...newOptions };
}
}

export { ArizonaSvelteLifecycle };
19 changes: 17 additions & 2 deletions priv/templates/arizona.svelte/assets/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ import ArizonaSvelte from './arizona-svelte.js';
globalThis.arizona = new Arizona({ logLevel: 'debug' });
arizona.connect({ wsPath: '/live' });

// Initialize ArizonaSvelte
// Initialize ArizonaSvelte with automatic monitoring
const arizonaSvelte = new ArizonaSvelte();
arizonaSvelte.mountComponents();

// Start automatic monitoring - components will mount/unmount automatically
arizonaSvelte.startMonitoring({
autoMount: true, // Automatically mount new components
autoUnmount: true, // Automatically unmount removed components
observeSubtree: true, // Monitor the entire DOM tree
debounceMs: 0 // Debounce DOM changes for 0ms
});

// Make available globally for debugging
globalThis.arizonaSvelte = arizonaSvelte;

// Add some helpful logging
console.log('[Arizona Svelte] 🚀 Automatic component monitoring started');
console.log('[Arizona Svelte] 🧪 LifecycleDemo component available in UI');
console.log('[Arizona Svelte] 🔍 Global access: window.arizonaSvelte');
Loading