@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 CHANGED
@@ -1,9 +1,11 @@
1
1
  # @csedl/svelte-on-rails
2
2
 
3
- It picks up svelte components rendered by [svelte-on-rails](https://gitlab.com/sedl/svelte-on-rails) rails gem and hydrates them.
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 {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
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
- startSvelteTags(componentsRoot, components, false);
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 the attributes
34
+ The helper from the `svelte-on-rails` gem adds attributes like
41
35
 
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={..}`
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 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"`
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
- export { hydrateComponents } from "./hydrateComponents";
3
+ // Store for tracking initialized Svelte component instances
4
+ const svelteInstances = new WeakMap();
3
5
 
4
- export { cleanupCustomElements, startSvelteTags } from "./mountCustomElements";
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.1",
4
- "description": "Hydrates svelte components and handles svelte custom elements without shadow-dom, that are rendered from ruby-gem svelte-on-rails.",
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"
@@ -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
- }
@@ -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 };