@csedl/svelte-on-rails 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -22
- package/index.js +119 -2
- package/package.json +2 -2
- package/hydrateComponents.js +0 -49
- package/mountCustomElements.js +0 -123
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# @csedl/svelte-on-rails
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Works together with the [svelte-on-rails](https://gitlab.com/sedl/svelte-on-rails) gem.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
First, you must have svelte and the gem running on your project, please follow the [gem](https://gitlab.com/sedl/svelte-on-rails) instructions.
|
|
8
|
+
|
|
7
9
|
```
|
|
8
10
|
npm i @csedl/svelte-on-rails
|
|
9
11
|
```
|
|
@@ -18,45 +20,37 @@ const componentsRoot = '/javascript'
|
|
|
18
20
|
|
|
19
21
|
// hydrate
|
|
20
22
|
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// build svelte custom elements
|
|
25
|
-
|
|
26
|
-
import { startSvelteTags, cleanupCustomElements } from '@csedl/svelte-on-rails';
|
|
27
|
-
startSvelteTags(componentsRoot, components, false); // debug: true für Logs
|
|
23
|
+
import {initializeSvelteComponents} from "../javascript/node-module";
|
|
24
|
+
initializeSvelteComponents(componentsRoot, components, true)
|
|
28
25
|
|
|
29
26
|
// Turbo-Event-Listener
|
|
30
27
|
document.addEventListener('turbo:load', () => {
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
document.addEventListener('turbo:before-cache', () => {
|
|
34
|
-
cleanupCustomElements(false); // debug: true für Logs
|
|
28
|
+
initializeSvelteComponents(componentsRoot, components, true);
|
|
35
29
|
});
|
|
36
30
|
```
|
|
37
31
|
|
|
38
32
|
## How it works
|
|
39
33
|
|
|
40
|
-
The helper from the `svelte-on-rails` gem adds
|
|
34
|
+
The helper from the `svelte-on-rails` gem adds attributes like
|
|
41
35
|
|
|
42
|
-
- `
|
|
43
|
-
-
|
|
44
|
-
- `data-
|
|
36
|
+
- class `svelte-on-rails-not-initialized-component`
|
|
37
|
+
- attribute `data-svelte-component="HelloWorld"`
|
|
38
|
+
- attribute `data-svelte-on-rails-initialize-action="hydrate"` if the element is SSR, otherwise `mount`
|
|
39
|
+
- attribute `data-props={items:['one','two','three']}`
|
|
45
40
|
|
|
46
41
|
to the wrapping element. Then, this lib
|
|
47
42
|
|
|
48
43
|
- picks up these elements
|
|
49
44
|
- picks the props, parses them by `JSON.parse(..)` and provides them to the element as svelte props
|
|
50
|
-
- hydrates
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- as custom element:
|
|
55
|
-
- adds the attribute `data-custom-element-initialized="my-component-file-name-svelte"`
|
|
45
|
+
- hydrates or mounts the element
|
|
46
|
+
- adds the attribute `data-svelte-initialized='true'`
|
|
47
|
+
- removes the class `svelte-on-rails-not-initialized-component`
|
|
48
|
+
- removes the attribute `data-svelte-on-rails-initialize-action`
|
|
56
49
|
|
|
57
50
|
## Requirements
|
|
58
51
|
|
|
59
52
|
- Svelte >= 5
|
|
53
|
+
- tested with ruby 3.2.2 and rails 7.1
|
|
60
54
|
|
|
61
55
|
## Testing
|
|
62
56
|
|
package/index.js
CHANGED
|
@@ -1,5 +1,122 @@
|
|
|
1
|
+
import { mount, hydrate, unmount } from 'svelte';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
// Store for tracking initialized Svelte component instances
|
|
4
|
+
const svelteInstances = new WeakMap();
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Initializes Svelte components on elements with the specified class.
|
|
8
|
+
* @param {string} componentsRoot - Root path for component files
|
|
9
|
+
* @param {Object} components - Object containing imported Svelte components
|
|
10
|
+
* @param {boolean} debug - Enable debug logging
|
|
11
|
+
*/
|
|
12
|
+
export function initializeSvelteComponents(componentsRoot, components, debug = false) {
|
|
5
13
|
|
|
14
|
+
const virginClass = 'svelte-on-rails-not-initialized-component';
|
|
15
|
+
const virginElements = document.getElementsByClassName(virginClass);
|
|
16
|
+
debugLog(`Found ${virginElements.length} elements with class «${virginClass}»`, debug);
|
|
17
|
+
if (virginElements.length === 0) return;
|
|
18
|
+
|
|
19
|
+
// Convert live HTMLCollection to a static array
|
|
20
|
+
const elementsArray = Array.from(virginElements);
|
|
21
|
+
|
|
22
|
+
// Iterate over the static array
|
|
23
|
+
|
|
24
|
+
for (const element of elementsArray) {
|
|
25
|
+
|
|
26
|
+
const action = element.getAttribute('data-svelte-on-rails-initialize-action');
|
|
27
|
+
const componentName = element.getAttribute('data-svelte-component');
|
|
28
|
+
|
|
29
|
+
// Check component
|
|
30
|
+
|
|
31
|
+
const componentPath = `${componentsRoot}/${componentName}.svelte`.replace(/\/+/g, '/');
|
|
32
|
+
if (!components[componentPath]) {
|
|
33
|
+
console.error(`[initializeSvelteComponents] Component not found for path: ${componentPath}`);
|
|
34
|
+
continue; // Proceed to the next element
|
|
35
|
+
}
|
|
36
|
+
const component = components[componentPath].default;
|
|
37
|
+
|
|
38
|
+
// Parse props
|
|
39
|
+
|
|
40
|
+
let props = {};
|
|
41
|
+
const attrKey = 'data-props';
|
|
42
|
+
const propsString = element.getAttribute(attrKey);
|
|
43
|
+
try {
|
|
44
|
+
if (propsString) props = JSON.parse(propsString);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error(`[initializeSvelteComponents] Error parsing ${attrKey} ("${propsString}") for ${componentName}:`, e);
|
|
47
|
+
continue; // Proceed to the next element
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
let instance;
|
|
52
|
+
if (action === 'mount') {
|
|
53
|
+
element.innerHTML = '';
|
|
54
|
+
instance = mount(component, {
|
|
55
|
+
target: element,
|
|
56
|
+
props,
|
|
57
|
+
hydrate: false,
|
|
58
|
+
});
|
|
59
|
+
debugLog2(`Mounted successfully ${componentName}, parsed props:`, props, debug);
|
|
60
|
+
} else if (action === 'hydrate') {
|
|
61
|
+
instance = hydrate(component, {
|
|
62
|
+
target: element,
|
|
63
|
+
hydrate: true,
|
|
64
|
+
props: props,
|
|
65
|
+
});
|
|
66
|
+
debugLog2(`Hydrated successfully ${componentName}, parsed props:`, debug);
|
|
67
|
+
} else {
|
|
68
|
+
console.error(`[initializeSvelteComponents] Unknown action: "${action}" for component ${componentName}`);
|
|
69
|
+
continue; // Proceed to the next element
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Store the instance for later cleanup
|
|
73
|
+
|
|
74
|
+
svelteInstances.set(element, instance);
|
|
75
|
+
|
|
76
|
+
element.classList.remove(virginClass);
|
|
77
|
+
element.setAttribute('data-svelte-initialized', true);
|
|
78
|
+
element.removeAttribute('data-svelte-on-rails-initialize-action');
|
|
79
|
+
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error(`[initializeSvelteComponents] Error ${action} ${componentName}:`, e);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cleans up all initialized Svelte component instances.
|
|
88
|
+
*/
|
|
89
|
+
export function cleanupSvelteComponents(debug = false) {
|
|
90
|
+
const initializedClass = 'svelte-initialized';
|
|
91
|
+
const initializedElements = document.querySelectorAll(`[data-${initializedClass}]`);
|
|
92
|
+
debugLog(`Cleaning up ${initializedElements.length} initialized Svelte components`, debug);
|
|
93
|
+
|
|
94
|
+
for (const element of initializedElements) {
|
|
95
|
+
try {
|
|
96
|
+
// In Svelte 5, use unmount to clean up the component
|
|
97
|
+
unmount(element);
|
|
98
|
+
debugLog2(`Unmounted Svelte component for element:`, element, debug);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(`[cleanupSvelteComponents] Error unmounting component for element:`, e);
|
|
101
|
+
// Fallback: Clear the element's content to ensure cleanup
|
|
102
|
+
element.innerHTML = '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Remove the instance from storage
|
|
106
|
+
svelteInstances.delete(element);
|
|
107
|
+
element.removeAttribute('data-svelte-initialized');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function debugLog(msg, debug) {
|
|
112
|
+
if (debug) {
|
|
113
|
+
console.log(`[initializeSvelteComponents] ${msg}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
function debugLog2(msg, object, debug) {
|
|
119
|
+
if (debug) {
|
|
120
|
+
console.log(`[initializeSvelteComponents] ${msg}`, object);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@csedl/svelte-on-rails",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Hydrates
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Hydrates or mounts svelte components that are rendered from ruby-gem svelte-on-rails.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.js"
|
package/hydrateComponents.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import {hydrate} from "svelte";
|
|
2
|
-
|
|
3
|
-
export function hydrateComponents(components_root, imported_components, debug = false) {
|
|
4
|
-
|
|
5
|
-
const selector = '[data-not-hydrated-svelte-component]';
|
|
6
|
-
const elements = document.querySelectorAll(selector);
|
|
7
|
-
|
|
8
|
-
if (elements.length === 0) {
|
|
9
|
-
debugLog(`found ${elements.length} elements with selector ${selector}, finishing process`, debug)
|
|
10
|
-
return
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
debugLog(`found ${elements.length} elements with selector ${selector}`, debug)
|
|
14
|
-
|
|
15
|
-
for (const path in imported_components) {
|
|
16
|
-
debugLog(`Having Svelte Path: «${path}»`, debug)
|
|
17
|
-
|
|
18
|
-
for (const element of elements) {
|
|
19
|
-
const filename = element.getAttribute('data-not-hydrated-svelte-component')
|
|
20
|
-
const check_path = `${components_root}/${filename}.svelte`.replace(/\/+/g, '/');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (check_path === path) {
|
|
24
|
-
debugLog(` ===> Found Svelte Filename: ${filename}`, debug)
|
|
25
|
-
const props = JSON.parse(element.getAttribute('data-props'));
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
hydrate(imported_components[path].default, {
|
|
29
|
-
target: element,
|
|
30
|
-
hydrate: true,
|
|
31
|
-
props: props,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
debugLog(`HYDRATED => ${filename}`, debug)
|
|
36
|
-
element.removeAttribute('data-not-hydrated-svelte-component')
|
|
37
|
-
element.setAttribute('data-svelte-hydrated', 'true')
|
|
38
|
-
debugLog(` ===> «${check_path}» match!`, debug)
|
|
39
|
-
} else {
|
|
40
|
-
debugLog(` ---> «${check_path}» does not match`, debug)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function debugLog(msg, debug) {
|
|
47
|
-
if (!debug) return;
|
|
48
|
-
console.log(`[hydrateComponents] ${msg}`)
|
|
49
|
-
}
|
package/mountCustomElements.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// @/javascript/node-module/mountCustomElements.js
|
|
2
|
-
import { mount, unmount } from 'svelte';
|
|
3
|
-
|
|
4
|
-
// Map for tracking mounted instances
|
|
5
|
-
const mountedInstances = new WeakMap();
|
|
6
|
-
|
|
7
|
-
// Function to clean up existing instances
|
|
8
|
-
function cleanupCustomElements(debug = false) {
|
|
9
|
-
if (debug) {
|
|
10
|
-
console.log('[mountCustomElements] Cleaning up existing custom elements');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
document.querySelectorAll('[data-custom-element-initialized]').forEach((element) => {
|
|
14
|
-
if (mountedInstances.has(element)) {
|
|
15
|
-
if (debug) {
|
|
16
|
-
console.log(`[mountCustomElements] Unmounting element: ${element.tagName.toLowerCase()}`);
|
|
17
|
-
}
|
|
18
|
-
unmount(mountedInstances.get(element));
|
|
19
|
-
mountedInstances.delete(element);
|
|
20
|
-
element.removeAttribute('data-custom-element-initialized');
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Function to format the tag name
|
|
26
|
-
function dashString(str) {
|
|
27
|
-
const a = str.replace(/([a-z])([A-Z])/g, '$1-$2'); // CamelCase to hyphen
|
|
28
|
-
const b = a.replace(/[\s_]+/g, '-'); // Spaces or underscores to hyphen
|
|
29
|
-
const d = b.toLowerCase(); // Convert to lowercase
|
|
30
|
-
const e = d.replace(/^-+|-+$/g, ''); // Remove leading or trailing hyphens
|
|
31
|
-
const f = e.replaceAll('/', '--'); // Slashes to double hyphens
|
|
32
|
-
return f + '-svelte';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Main function to initialize Svelte tags
|
|
36
|
-
function initializeSvelteTags(componentsRoot, components, debug = false) {
|
|
37
|
-
if (debug) {
|
|
38
|
-
console.log('[mountCustomElements] Initializing custom elements with root:', componentsRoot);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Clean up old instances
|
|
42
|
-
cleanupCustomElements(debug);
|
|
43
|
-
|
|
44
|
-
// Iterate over all imported components
|
|
45
|
-
for (const path in components) {
|
|
46
|
-
const component = components[path].default;
|
|
47
|
-
|
|
48
|
-
// Generate tag names from the path
|
|
49
|
-
const regex = new RegExp(`^${componentsRoot}(/*)`);
|
|
50
|
-
const tagName = dashString(path.replace(regex, '').replace(/\.svelte$/, ''));
|
|
51
|
-
|
|
52
|
-
// Check if tagName is valid (must contain at least one hyphen)
|
|
53
|
-
if (!tagName || !tagName.includes('-')) {
|
|
54
|
-
console.error(`[mountCustomElements] Invalid tag name: ${tagName} for path: ${path}`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (debug) {
|
|
59
|
-
console.log(`[mountCustomElements] Processing component: ${tagName}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Find all elements with the generated tag name
|
|
63
|
-
const elements = document.querySelectorAll(`${tagName}:not([data-custom-element-initialized])`);
|
|
64
|
-
|
|
65
|
-
elements.forEach((element) => {
|
|
66
|
-
if (debug) {
|
|
67
|
-
console.log(`[mountCustomElements] Found element: ${tagName}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Read data-props attribute
|
|
71
|
-
const propsString = element.getAttribute('data-props');
|
|
72
|
-
let props = {};
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// Parse JSON from data-props
|
|
76
|
-
if (propsString) {
|
|
77
|
-
props = JSON.parse(propsString);
|
|
78
|
-
if (debug) {
|
|
79
|
-
console.log(`[mountCustomElements] Parsed props for ${tagName}:`, props);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch (e) {
|
|
83
|
-
console.error(`[mountCustomElements] Error parsing data-props for ${tagName}:`, e);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Clear the element to ensure no old content remains
|
|
87
|
-
element.innerHTML = '';
|
|
88
|
-
|
|
89
|
-
// Initialize the Svelte component with mount
|
|
90
|
-
try {
|
|
91
|
-
if (debug) {
|
|
92
|
-
console.log(`[mountCustomElements] Mounting ${tagName}`);
|
|
93
|
-
}
|
|
94
|
-
const instance = mount(component, {
|
|
95
|
-
target: element, // Render directly into the element (no Shadow DOM)
|
|
96
|
-
props: props || {}, // Pass parsed props or empty object
|
|
97
|
-
hydrate: false, // No hydration
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Store the instance and mark the element
|
|
101
|
-
mountedInstances.set(element, instance);
|
|
102
|
-
element.setAttribute('data-custom-element-initialized', tagName);
|
|
103
|
-
|
|
104
|
-
if (debug) {
|
|
105
|
-
console.log(`[mountCustomElements] Successfully mounted ${tagName}`);
|
|
106
|
-
}
|
|
107
|
-
} catch (e) {
|
|
108
|
-
console.error(`[mountCustomElements] Error mounting ${tagName}:`, e);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Exported function to start the initialization
|
|
115
|
-
export function startSvelteTags(componentsRoot, components, debug = false) {
|
|
116
|
-
if (debug) {
|
|
117
|
-
console.log('[mountCustomElements] Starting custom elements initialization');
|
|
118
|
-
}
|
|
119
|
-
initializeSvelteTags(componentsRoot, components, debug);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Export cleanupCustomElements for manual cleanup (e.g., for turbo:before-cache)
|
|
123
|
-
export { cleanupCustomElements };
|