@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 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,5 @@
1
+
2
+ export { hydrateComponents } from "./hydrateComponents";
3
+
4
+ export { cleanupCustomElements, startSvelteTags } from "./mountCustomElements";
5
+
@@ -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
+ }