@csedl/svelte-on-rails 0.0.1
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 +68 -0
- package/hydrateComponents.js +49 -0
- package/index.js +5 -0
- package/mountCustomElements.js +123 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @csedl/svelte-on-rails
|
|
2
|
+
|
|
3
|
+
It picks up svelte components rendered by [svelte-on-rails](https://gitlab.com/sedl/svelte-on-rails) rails gem and hydrates them.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @csedl/svelte-on-rails
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage example with Rails/turbo/vite
|
|
12
|
+
|
|
13
|
+
A file like `initializeSvelte.js` that is imported into your entrypoint file
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
const components = import.meta.glob('/javascript/**/*.svelte', { eager: true })
|
|
17
|
+
const componentsRoot = '/javascript'
|
|
18
|
+
|
|
19
|
+
// hydrate
|
|
20
|
+
|
|
21
|
+
import {hydrateComponents} from "@csedl/svelte-on-rails";
|
|
22
|
+
hydrateComponents(componentsRoot, components, false) // debug: true für Logs
|
|
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
|
|
28
|
+
|
|
29
|
+
// Turbo-Event-Listener
|
|
30
|
+
document.addEventListener('turbo:load', () => {
|
|
31
|
+
startSvelteTags(componentsRoot, components, false);
|
|
32
|
+
});
|
|
33
|
+
document.addEventListener('turbo:before-cache', () => {
|
|
34
|
+
cleanupCustomElements(false); // debug: true für Logs
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
The helper from the `svelte-on-rails` gem adds the attributes
|
|
41
|
+
|
|
42
|
+
- `data-not-hydrated-svelte-component=myComponentFileName` if the component is rendered server side («SSR») (usually on initial request)
|
|
43
|
+
- renders a tag like `my-component-file-name-svelte` if the component is rendered for a custom element (on hotwired/turbo request)
|
|
44
|
+
- `data-props={..}`
|
|
45
|
+
|
|
46
|
+
to the wrapping element. Then, this lib
|
|
47
|
+
|
|
48
|
+
- picks up these elements
|
|
49
|
+
- picks the props, parses them by `JSON.parse(..)` and provides them to the element as svelte props
|
|
50
|
+
- hydrates the element or initializes the custom element
|
|
51
|
+
- SSR:
|
|
52
|
+
- removes the `data-not-hydrated-svelte-component` attribute (in case of SSR)
|
|
53
|
+
- adds the attribute `data-svelte-hydrated='true'`
|
|
54
|
+
- as custom element:
|
|
55
|
+
- adds the attribute `data-custom-element-initialized="my-component-file-name-svelte"`
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Svelte >= 5
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
|
|
63
|
+
I developed this all on Rails-7, together with `vite_rails`.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
There is a [rails project](https://gitlab.com/sedl/svelte-on-rails-tests) with testings (based on playwright) where the gem and this package are tested together.
|
|
67
|
+
|
|
68
|
+
The gem includes its own tests.
|
|
@@ -0,0 +1,49 @@
|
|
|
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/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
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 };
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@csedl/svelte-on-rails",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Hydrates svelte components and handles svelte custom elements without shadow-dom, that are rendered from ruby-gem svelte-on-rails.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://gitlab.com/sedl/csedl-svelte-on-rails.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"svelte",
|
|
18
|
+
"rails"
|
|
19
|
+
],
|
|
20
|
+
"author": "Christian Sedlmair",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": ""
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://gitlab.com/sedl/csedl-svelte-on-rails"
|
|
26
|
+
}
|