svelte-on-rails 6.0.1 → 7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a07c9c33f2ef2d71acbd9986cc841fada803a3692abec84719648e9d5d94c79
4
- data.tar.gz: 8677455b7ef6137269451b12f9ed8350ff7ba842198f89e2864f7e6c5615a20e
3
+ metadata.gz: 6b61dc717ae11cdc216f2ce8a35239826d075e7fdb04559ac36f124d912ec593
4
+ data.tar.gz: 9abd85b4352eda996544904ce4df984773ebedbdf580a1c8d4aec59ab31c1481
5
5
  SHA512:
6
- metadata.gz: 9a97fd9a881d5fa6b32f4faacf87ded1210939984e01274b4fb334b6f5444624ede7dac476e8fa2daa83f960d2f34ecacdc49cbebb4d0ed548c05f5425900622
7
- data.tar.gz: 1caab439e622e43009cbc1ff2ee193fa35e0bbc0b63128ca0c917787536a01d3e92910f52e110fcec38d17533e6e7c7415d05ce87970d07fd9f45287801c8355
6
+ metadata.gz: 44935ca3cca431137508364778578b588fb0bf3339315823b975c98995640c46e8fd67fe05bc92a5cea7233a1ea1be2bcd540bc1a0955513e667f3bda283fe38
7
+ data.tar.gz: a778b7fbc71ad035c93e08cf99ba559c67fc18c8bd6cb717583e35a092c57cef8bc8a99c97ce26ba0fc98fdbe53d907d5ef67a45a8bff5458f48ca963c18c96d
data/README.md CHANGED
@@ -11,27 +11,27 @@ Realizing [DHH's vision](https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a
11
11
 
12
12
  Svelte offers the simplest and most elegant soulution to building reactive, high-performance front-end components.
13
13
 
14
- - **Seamless Integration**
15
- - Works flawlessly with Hotwired/Turbo
16
- - Enhances Hotwire’s capabilities
17
- - **Developer-Friendly**
18
- - Simple to learn, intuitive, and powerful
19
- - Lightning-fast performance
20
14
  - **Compared to Single Page Apps (SPAs)**
21
15
  - Full-stack development delivers maximum value:
22
16
  - Unified testing from database to frontend
23
17
  - Single-source system delivery
24
18
  - For the most HTML Hotwired is enough
19
+ - **Compared to integrated React or Vue**
20
+ - No virtual DOM, resulting in leaner packages and faster performance
21
+ - See Rich Harris’ [Rethinking Reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity) (3:50–6:40) for a compelling comparison
22
+ - Easier to learn
23
+ - While React and Vue have larger communities, Svelte’s ecosystem is robust and growing, ideal for Rails integration
25
24
  - **Compared to Hotwired**
26
- - Stimulus is not a tool for frontend-apps
25
+ - Stimulus is a initializer, but not a tool for frontend-apps!
27
26
  - Svelte eliminates redundant HTML initial state logic
28
27
  - Consolidates component logic into a single file
29
28
  - Offloads rendering to JavaScript by Frontend, Server side Rendering only where necessary, reducing server load
30
- - **Compared to React/Vue**
31
- - No virtual DOM, resulting in leaner packages and faster performance
32
- - See Rich Harris[Rethinking Reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity) (3:50–6:40) for a compelling comparison
33
- - Easier to learn
34
- - While React and Vue have larger communities, Svelte’s ecosystem is robust and growing, ideal for Rails integration
29
+ - **Seamless Integration**
30
+ - Works flawlessly with Hotwired/Turbo
31
+ - Enhances Hotwires capabilities
32
+ - **Developer-Friendly**
33
+ - Simple to learn, intuitive, and powerful
34
+ - Lightning-fast performance
35
35
 
36
36
  Svelte empowers Rails’ full-stack vision with modern, efficient front-end integration.
37
37
 
@@ -46,112 +46,34 @@ Svelte empowers Rails’ full-stack vision with modern, efficient front-end inte
46
46
 
47
47
  see [issues](https://gitlab.com/sedl/svelte-on-rails/-/issues)
48
48
 
49
- # Svelte on Rails 👍
50
-
51
- Rock-solid and seamless integration of Svelte Components into Rails views, based on `vite_rails`.
52
-
53
- By default, and when installed together with `@hotwired/turbo-rails`, it renders
54
- svelte components on the first request server side («SSR») and for subsequent
55
- requests it provides a empty tag which is mounted on the frontend
56
- by the associated npm package [@csedl/svelte-on-rails](https://www.npmjs.com/package/@csedl/svelte-on-rails).
57
-
58
- This way svelte works perfectly together with turbo. You will never notice
59
- this unpleasant «blink» on the frontend while the whole process is maximum
60
- performance optimized.
49
+ # Description 👍
61
50
 
62
- Thanks to [shakacode](https://github.com/shakacode/react_on_rails)
63
- and [ElMassimo](https://github.com/ElMassimo) for inspiring and helping me with their gems.
51
+ Renders Svelte components server-side and, together with [@csedl/svelte-on-rails](https://www.npmjs.com/package/@csedl/svelte-on-rails),
52
+ hydrates the component on the frontend.
64
53
 
65
- **STATUS:** This gem is in the early stages of development, but is ready for use.
66
- It has nearly 100% test coverage, with all tests passing. It has been in use since May/June 2025
67
- on the systems of my two biggest customers.
54
+ Together with `turbo-rails` or `turbolinks`, if configured, it renders server-side only on initial requests
55
+ and delivers a empty tag that will be rendered by frontend.
68
56
 
69
57
  If you have issues, please open one, and contributors are welcome!
70
58
 
71
59
  ## Requirements
72
60
 
73
- - actual node installed on the server
74
- - tested on
75
- - ruby 3.2.2 and rails 7.1
76
- - ruby 2.7.5 and rails 6.1
77
- - vite@6 (v7 not supported, see issues)
78
- - turbolinks and hotwired/turbo
61
+ - tested on
62
+ - ruby 3.2.2 and rails 7.1
63
+ - ruby 2.7.5 and rails 6.1
64
+ - vite@6 (v7 not supported, see issues)
65
+ - turbolinks and hotwired/turbo
79
66
  - vite_rails (the installer will install it by option --full or --vite)
80
- - svelte@5, vite-plugin-svelte@5 (see: [how to install svelte on rails/vite](https://dev.to/chmich/setup-inertia-and-svelte-on-rails-7-3glk))
67
+ - svelte@5, @sveltejs/vite-plugin-svelte@5 (see: [how to install svelte on rails/vite](https://dev.to/chmich/setup-inertia-and-svelte-on-rails-7-3glk))
81
68
  - turbo (recommended / [how to install turbo on rails](https://github.com/hotwired/turbo-rails?tab=readme-ov-file#installation))
82
- - if you use special packages (like pug) that requires commonjs, you may need
83
69
  - npm on latest versions
84
- - actual node installed.
85
- - if `nvm` is installed it gets the path to the node-binary from there.
86
- - When `.nvmrc` is present on projects root, it is respected
87
- - If node is not included on the PATH you can configure your node path by environment variable `SVELTE_ON_RAILS_NODE_BIN`
70
+ - actual node installed.
71
+ - if `nvm` is installed it gets the path to the node-binary from there.
72
+ - When `.nvmrc` is present on projects root, it is respected
73
+ - If node is not included on the PATH you can configure your node path by environment variable `SVELTE_ON_RAILS_NODE_BIN`
88
74
 
89
- ## Installation from cero ⚙️
90
75
 
91
- ```bash
92
- rails new my-test-app --skip-javascript
93
- ```
94
-
95
- within the app add the gem
96
-
97
- ```bash
98
- bundle add svelte-on-rails
99
- ```
100
-
101
- and run the installer
102
-
103
- ```bash
104
- rails g svelte_on_rails:install --full
105
- ```
106
-
107
- You have done it! 👍
108
-
109
- Start the server and you will see a svelte hello world component rendered on the browser.
110
-
111
- **Explanation:**
112
-
113
- The `--full` contains:
114
-
115
- - `--vite`
116
- - adds vite_rails gem and running the installer
117
- - `--haml`
118
- - adds the gem and converts existing views
119
- - `svelte_on_rails`
120
- - This is not a option, this is always done: Adds a config file and installs `@csedl/svelte-on-rails` by npm
121
- - `--turbo`
122
- - installs `@hotwired/turbo-rails` and adds import statement to application.js
123
- - `--svelte`
124
- - adds or updates `svelte`
125
- - `--hello-world`
126
- - adds a hello world component
127
-
128
- You can also use the options you want instead of using `--full`.
129
- And there is the option `--force` that would not ask you whether it should overwrite existing files, for automated processes like testing.
130
-
131
- This means, if you want all, except haml, you can run:
132
-
133
- ```bash
134
- rails g svelte_on_rails:install --vite --turbo --svelte --hello-world
135
- ```
136
-
137
- The installer is written carefully: It does not overwrite a file without asking
138
- and tells you all what he is doing.
139
-
140
- At the end, you just have to (re-) start your server
141
- and you will see a Svelte Hello World component.
142
-
143
- This hello world has a second page where the most common import statements are demonstrated,
144
- separated for server and client side rendering.
145
-
146
- Then, you can run
147
-
148
- ```bash
149
- rails svelte_on_rails:remove_hello_world
150
- ```
151
-
152
- and start coding.
153
-
154
- ## Installation on a existing app ⚙️
76
+ ## Installation
155
77
 
156
78
  Required: `vite_rails` must be installed, it wants a `app/frontend` folder.
157
79
 
@@ -182,533 +104,12 @@ Restart the server, add a hello world component `app/frontend/javascript/HelloWo
182
104
  Add it to the view
183
105
 
184
106
  ```erb
185
- <%= svelte_component('HelloWorld', title: 'Hello World') %>
107
+ <%= svelte_component('HelloWorld', {title: 'Hello World'}) %>
186
108
  ```
187
109
 
188
-
189
110
  And you should see "Svelte Hello World" on the browser! 👍 🤗
190
111
 
191
- **Explanation**
192
-
193
- this Minimal installer does:
194
-
195
- - add `app/frontend/initializers/svelte.js`
196
- - Adds a import statement for that initializer to `application.js`
197
- - add `app/frontend/ssr/ssr.js`
198
- - add `config/svelte_on_rails.yml`
199
- - add `vite-ssr.config.ts`
200
- - add command `npm run build:ssr` to package.json
201
- - installs or updates npm packages to the latest:
202
- - `@csedl/svelte-on-rails`
203
- - `typescript`
204
- - `@types/node`
205
-
206
- **Troubleshooting**
207
-
208
- In the first step, the installer runs the backend parts that are unlikely to fail.
209
- Then it installs the npm packages that are more likely to fail because of dependencies.
210
- If so, just check that all the
211
- packages are installed on the latest versions and you should be fine. 🤓
212
-
213
- The most importand rule is to firstly check all npm packages installed and passing before changing your view logik.
214
-
215
- ## Check if it all works
216
-
217
- Server Side Rendering (SSR) is a parallel pipeline to client side rendering.
218
- Both should return the same HTML. And your global styles should be applied same way
219
- for both cases. For normal use cases this is.
220
-
221
- For check the **ssr pipeline** you can pass the options `ssr: true` and `hydrate: false`
222
- to the view helper. This way you will see a «dead» backend rendered HTML with no javascript applied.
223
-
224
- For check the **client side pipeline** you can pass the option `ssr: false` and
225
- `hydrate: true` to the view helper.
226
-
227
- If both are looking similar, you are good to go. Then, remove theese options, the defaults are
228
- `ssr: :auto` and `hydrate: true`.
229
-
230
- ### Import statements
231
-
232
- The most importand import statements that are served by this gem are included in the
233
- hello world component and by that they are also within the testing scope. So you can
234
- be sure that they are working. If importand statements are missing there, pelase
235
- tell me.
236
-
237
- Among others, working statements are:
238
-
239
- - `import svg from '../example.svg?raw'`
240
- - `<script lang="ts">` (svelte component with typescript syntax)
241
- - `import { someFunction } from '../customJavascript.js';`
242
- - `import Child from './Child.svelte';`
243
-
244
- ### Precompile assets
245
-
246
- Usual vite has a `vite.config.ts` file, that is used for the client side precompilation.
247
-
248
- By running this installer it adds `vite-ssr.config.ts` and a npm runner so that you can do `npm run build:ssr`
249
- which does the server side precompilation.
250
-
251
- The same job is triggered alongside `rails assets:precompile` for production environments.
252
-
253
- On development, when `watch_changes` is configured, the precompilation is triggered
254
- after any `*.svelte` file within the configured `components_folder` changed.
255
-
256
- ## Option `ssr: :auto`
257
-
258
- `ssr: :auto` is the default option, as set on config file and can be overridden on the view helper.
259
-
260
- By passing the `ssr: :auto` option to the view helper,
261
- it checks if the request is an initial request (request header `X-Turbo-Request-ID` is present // configurable by `non_ssr_request_header`):
262
-
263
- On Initial-Request, it adds the attribute `data-svelte-on-rails-initialize-action="hydrate"` and
264
- returns a server side rendered component that will be hydrated on the frontend by the npm package.
265
-
266
- Otherwise it adds `mount` instead of hydrate, and renders a empty element, but provided with the
267
- necessary attributes for the npm package.
268
-
269
- More details to this point you can find on the npm package.
270
-
271
- This works perfectly with hotwired/turbo because the javascript is only
272
- loaded on very first load to the frontend, then the most work is done
273
- in frontend and the server is relieved, except on initial request.
274
- You will see no unpleasant «blink» on the page.
275
-
276
- **Turbolinks**
277
-
278
- If you are working on turbolinks, you can config the header or set something like
279
-
280
- ```
281
- document.addEventListener('turbolinks:request-start', (event) => {
282
- var xhr = event.data.xhr
283
- xhr.setRequestHeader("X-Turbo-Request-ID", "any-content-for-skip-svelte-ssr")
284
- });
285
- ```
286
-
287
- to your javascript file.
288
-
289
-
290
- **Tip: Performance optimisation for dropdowns**
291
-
292
- For example, with dropdowns, you will never see the unpleasant «blink» effect because
293
- the Svelte component is not visible for the first moment after rendering.
294
- Server-side rendering is unnecessary here. You can pass 'ssr: false' to the view helper.
295
- This relieves the server and reduces loading time.
296
-
297
- **Tip: Testing**
298
-
299
- Consider setting `ssr: false` for testing only for performance reasons.
300
- Yo can override this on a attribute on the view-helper for specific components.
301
- But, in normal cases it should not be neccessary testing ssr explicitly.
302
-
303
- ## ActiveRecord helpers
304
-
305
- Adds the `#to_svelte` helper to your models, example:
306
-
307
- ```ruby
308
- @book.to_svelte(
309
- :name,
310
- :calculation_method,
311
- author: [:name],
312
- editions: [
313
- :date,
314
- offset: 2,
315
- limit: 1
316
- ]
317
- )
318
- ```
319
-
320
- would result in something like this:
321
-
322
- ```ruby
323
- {
324
- "values" => {
325
- "name" => "Learning Ruby",
326
- "calculation_method": "any-result",
327
- "author" => {
328
- "name" => "Michael Hartl"
329
- },
330
- "editions" => [
331
- {
332
- date: "2025-02-03"
333
- }
334
- ]
335
- },
336
- "book_labels" => {
337
- "name" => "Name", # translated by human_attribute_name
338
- "calculation_method" => "Calculation method",
339
- "author" => "Author"
340
- },
341
- "author_labels": {
342
- "name" => "Name"
343
- },
344
- "edition_labels" => {
345
- "date" => "Date"
346
- }
347
- }
348
- ```
349
-
350
- This should ease transferring data you need within the component mostly.
351
-
352
- The same method is applicable for:
353
-
354
- - The model itself
355
- - It returns only the labels: Same result like above, but without the `values` key
356
- - `ActiveRecord::Relation`
357
- - Same result like above, but `values` then is a Array.
358
-
359
- If a association returns a empty result, the labels are still calculated.
360
-
361
- `offset` and `limit` are reserved keys, so, columns with the same name would be ignored.
362
-
363
- **Caching:**
364
-
365
- Caching Capability is not implemented on this method, you easily can wrap it by `Redis`
366
-
367
- If used on the `cached_svelte_component` view helper,
368
- the component's attributes are used to generate a checksum, which serves as the
369
- cache key for efficient storage and retrieval. So, this method is meant to
370
- make it easier to exactly filter out only the information that is needed
371
- on the component.
372
-
373
- ## Caching
374
-
375
- Caching only is relevant for `ssr`
376
-
377
- When using the `cached_svelte_component` helper you must have the `redis` gem installed.
378
-
379
- This caches on a key like `svelte-on-rails:development:SvelteOnRailsHelloWorld.svelte-1xq5tnu-User1:fscyhz-18bm76a` which includes:
380
-
381
- - Namespace, if configured, otherwise the default is gem-name and environment
382
- - component filename
383
- - checksum of the file-path
384
- - `cache_key` if given as argument to the view-helper
385
- - can be a array of active-record objects or strings or a single element instead of a array
386
- - checksum of the last modification timestamp of the component (only when `watch_changes` is set to true)
387
- - checksum of the given attributes
388
-
389
- **Configuration**
390
-
391
- Like usually you can configure your cache store on your environment by something like:
392
-
393
- ```ruby
394
- config.cache_store = :redis_cache_store, { url: 'redis://localhost:6379/2',
395
- expires_in: 90.minutes,
396
- namespace: 'my-example-app' }
397
- ```
398
-
399
- And you can override this by
400
-
401
- ```yaml
402
- redis_cache_store:
403
- expires_in: 2.hours
404
- ```
405
-
406
- on the svelte-on-rails config file or pass the `expires_in` as argument to the view helper.
407
-
408
- **Check if it works**
409
-
410
- Pass `debug: true` to the helper and you will see on the logs how your configuration works.
411
-
412
- ## ActionCable vs. TurboStream
413
-
414
- There are two ways that the server can talk to the client over Websocket:
415
-
416
- - **ActionCable** transmits directly to the frontends javascript
417
- - **TurboStreams** is a wrapper around ActionCable
418
- - You always need a html part for communication by secured channels
419
- - Makes sense when you want to transfer confidential data or separate onto privileged user channels
420
- - Has [compatibility issues with Rails-UJS](https://github.com/hotwired/turbo-rails?tab=readme-ov-file#compatibility-with-rails-ujs)
421
-
422
-
423
- ## SvelteOnRails::ActionCable
424
-
425
- **Setup**
426
-
427
- Add `app/channels/svelte_on_rails_channel.rb`
428
-
429
- ```ruby
430
- class SvelteOnRailsChannel < ApplicationCable::Channel
431
- def subscribed
432
- stream_from "svelte_on_rails_channel"
433
- end
434
-
435
- def unsubscribed
436
- # Any cleanup needed when channel is unsubscribed
437
- end
438
- end
439
- ```
440
-
441
- config
442
-
443
- ```yaml
444
- action_cable:
445
- channel: "svelte_on_rails_channel"
446
- ```
447
-
448
- javascript
449
-
450
- ```shell
451
- npm i @rails/actioncable
452
- ```
453
-
454
- Add to `application.js`
455
-
456
- ```javascript
457
- import { createConsumer } from "@rails/actioncable"
458
- import { SvelteOnRails, dispatchSvelteStreamEvent, actionCableDebugLog } from '@csedl/svelte-on-rails'
459
- SvelteOnRails.debug = true
460
-
461
- const consumer = createConsumer()
462
-
463
- consumer.subscriptions.create("SvelteOnRailsChannel", {
464
- connected() {
465
- actionCableDebugLog("Connected to SvelteOnRailsChannel")
466
- },
467
- disconnected() {
468
- actionCableDebugLog("Disconnected from SvelteOnRailsChannel")
469
- },
470
- received(data) {
471
- actionCableDebugLog("Received:", data)
472
- dispatchSvelteStreamEvent(data)
473
- }
474
- })
475
- ```
476
-
477
- **What dispatchSvelteStreamEvent does**
478
-
479
- - Without the attribute `component` given,
480
- it searches for all elements with the class `svelte-component` and fires the event `channel-action`
481
- - When `selector` is given, it searches for all matching elements within each component.
482
- - The event can be overriden by the argument `event`
483
-
484
- **Usage**
485
-
486
- `app/frontend/javascript/components/folder/MyComponent.svelte`
487
-
488
- ```sveltehtml
489
- <script>
490
- import {addComponentStreamListener} from '@csedl/svelte-on-rails/src/componentStreamListener'
491
-
492
- function handleCableEvent(event) {
493
- console.log('Event received by Turbo Stream', event.detail)
494
- }
495
- </script>
496
- <!--on ANY element:-->
497
- <h1 use:addComponentStreamListener={handleCableEvent}>Test TurboStreams Channel</h1>
498
- ```
499
-
500
- The `addComponentStreamListener` adds the eventListener `stream-action` on the wrapping Element.
501
- The «wrapping Element» is the Element from the view helper `svelte_component` with the class `svelte-component`.
502
-
503
- Now you can dispatch events on the component by:
504
-
505
- ```ruby
506
- SvelteOnRails::ActionCable.dispatch(
507
- 'folder/MyComponent',
508
- { message: "greetings from Server: äöü🤣🌴🌍漢字" }
509
- )
510
- ```
511
-
512
- And you will find the object, with the message key on the browser logs.
513
-
514
- Without any arguments, just by `SvelteOnRails::ActionCable.dispatch` it would fire the `stream-action` event on all components.
515
-
516
- The **#dispatch_by_selector** does not go over the component, it searches for any matching selector just
517
- on the whole `document` and fires the given event there.
518
-
519
- # SvelteOnRails::TurboStream
520
-
521
- **Setup**
522
-
523
- Please setup the `turbo-rails` gem and follow the chapter [Come alive with Turbo Streams](https://github.com/hotwired/turbo-rails?tab=readme-ov-file#come-alive-with-turbo-streams), which mainly is:
524
-
525
- ```shell
526
- bundle add turbo-rails
527
- rails turbo:install
528
- ```
529
-
530
- make sure that `import "@hotwired/turbo-rails"` is on your application.js and set the view helper
531
- `<%= turbo_stream_from 'authenticated' if current_user %>` to your view.
532
-
533
- If a channel (e.g.: `authenticated`) is active and you have an HTML element with a HTML-ID (e.g.: `svelte-on-rails-stream-actions-box`),
534
- you can test it by:
535
-
536
- ```ruby
537
- Turbo::StreamsChannel.send(
538
- "broadcast_append_to",
539
- 'authenticated',
540
- target: 'svelte-on-rails-stream-actions-box',
541
- content: "<div>Turbo-Streams are working!</div>"
542
- )
543
- ```
544
-
545
- When this works you are good to go.
546
-
547
- **Configs**
548
-
549
- Check the regarding keys and their commends on the [config file](https://gitlab.com/sedl/svelte-on-rails/-/blob/main/templates/config_base/config/svelte_on_rails.yml?ref_type=heads).
550
- From there, you just need:
551
-
552
- ```yaml
553
- turbo_stream:
554
- target_html_id: 'svelte-on-rails-stream-actions-box'
555
- channel: 'public'
556
- ```
557
-
558
- **Minimal Usage Example**
559
-
560
- And call this by:
561
-
562
- ```ruby
563
- SvelteOnRails::TurboStream.dispatch
564
- ```
565
- **What it does**
566
-
567
- - A Stimulus controller is pushed to all subscribers of the configured channel.
568
- - You can override the channel name by passing `channel` to the method.
569
-
570
- **Usage Example, more detailed**
571
-
572
- If you want to fire a event on a specific element within the component, you do not need `addComponentStreamListener`.
573
- Just do something like:
574
-
575
- ```sveltehtml
576
- <script>
577
- function logStreamMessage(event) {
578
- console.log(event.detail.message)
579
- }
580
- </script>
581
- <button class="counter-button" onclick="{logStreamMessage}">Show Contents</button>
582
- ```
583
-
584
- within the svelte component.
585
-
586
- Then, call it by:
587
-
588
- ```ruby
589
- SvelteOnRails::TurboStream.dispatch(
590
- channel: 'my-custom-stream',
591
- component: '/javascript/components/TurboStreamsChannel',
592
- selector: '.counter-button',
593
- event: 'click',
594
- event_detail: { message: "Greetings from Server: äöü🤣🌴🌍漢字" }
595
- )
596
- ```
597
-
598
- The **#dispatch_by_selector** does not go over the component, it searches for any matching selector just
599
- on the whole `document` and fires the given event there.
600
-
601
- ## More rake tasks
602
-
603
- This tasks are more for testing/playground purposes
604
-
605
- ```bash
606
- rails svelte_on_rails:add_hello_world
607
- ```
608
-
609
- ```bash
610
- rails svelte_on_rails:remove_hello_world
611
- ```
612
-
613
- ## Deploy
614
-
615
- For example, by deploying with capistrano, you may add a step like
616
-
617
- ```ruby
618
- before 'deploy:assets:precompile', 'deploy:npm:install'
619
- namespace :deploy do
620
- namespace :npm do
621
- desc 'Install Node.js dependencies'
622
- task :install do
623
- on roles(:app) do
624
- within release_path do
625
- execute :npm, 'install'
626
- end
627
- end
628
- end
629
- end
630
- end
631
- ```
632
-
633
- to `deploy.rb` for making sure the ssr compilation is working.
634
-
635
- ## Contributors Guide
636
-
637
- Contributors welcome!
638
-
639
- **Download the code and run the tests**
640
-
641
- Download the source code from the repository, and within the project folder run:
642
-
643
- ```bash
644
- rake svelte_on_rails:create_contributor_configs_file
645
- ```
646
- and define a `generated_test_app_folder_path` (required) for apps, generated for the testings.
647
-
648
- For development of the **npm package @csedl/svelte-on-rails** (optional) please download the source code of the
649
- npm package and set `local_npm_package_path` on the config file to the path to the npm package on your local machine.
650
- This will cause the installer, to install the npm package from a local path instead from the npm registry.
651
-
652
- Then run the tests and start contributing.
653
-
654
- **RUN THE FIRST TEST (!)**: Testing is complex here because of the design based on the installer test.
655
- On Problems, i always run the `Installer > destroy and create rails app > FIRST TEST > [...] check if javascript works`.
656
- If there are problems, open the generated app on a IDE and check errors there.
657
-
658
- When this passes, all the others passing mostly.
659
-
660
- At the end of the most tests it leaves the rails server running, so that you can see the result on `localhost:3000`.
661
-
662
- NOTE: Theese tests are dependend on your environment, including the running ruby version!
663
- I am working on rvm. If you work on a different environment, some (not many) changes may be necessary.
664
- Thats your part :)
665
-
666
- The current test cases including (among others):
667
-
668
- create a completely new rails app, running the full installer and check if a hello World component is visible and javascript is working.
669
- run assets:precompile within a rails app and check if the gem does its precompiling too.
670
-
671
- ## Understanding the process
672
-
673
- **Why not client and server rendering in one process?**
674
-
675
- That was my idea! `application.js` which is the usual entry point for the client
676
- side could live on the same assets and manifest like svelte components that are
677
- compiled as chunks which each is its own entry point. This failed:
678
-
679
- - The `vite-plugin-ruby` did not support this: it constrained all to one entry point.
680
- - See how fat the `vite-ssr.config.ts` is. For the client side this is not necessary.
681
-
682
- At the end, I decided to split the process. For now it is cleaner. But that is not the last decision.
683
-
684
- **Why not compiling server side purely by rollup?**
685
-
686
- Advantages would be much slimmer packages and faster compilation. On the first
687
- step i did this and i backed up a working and tested code on the branch [rollup-ssr](https://gitlab.com/sedl/svelte-on-rails/-/blob/rollup-ssr/BRANCH_INFO.md?ref_type=heads).
688
-
689
- I decided to use Vite to bring the client and server side rendering
690
- closer together.
691
-
692
- But this, too, is not the last decision.
693
-
694
- For now we proceed with vite.
695
-
696
- **How does it work now?**
697
-
698
- Client side rendering is done by vite like usual.
699
-
700
- Server side rendering is triggered, similar to [vite_rails](https://vite-ruby.netlify.app/guide/deployment.html)
701
- on `assets:precompile`, and, if `watch_changes` is configured,
702
- which is default for development, it is triggered
703
- on every change of a `*.svelte` file within the configured `components_folder`.
704
-
705
- On the server side only the `*.svelte` files are served. Theyr included
706
- assets are linked to the client side assets folder, which is mapped by `manifest.json`.
707
-
708
- Then, vite has two output folders: `vite-dev` for development and `vite` for production.
709
- Within `vite-ssr.config.ts`, by the `RAILS_ENV` variable, is decided which one is used.
710
-
711
112
 
712
113
  ## Licence
713
114
 
714
- License is MIT
115
+ Copyright © 2025-2028 sedlmair.ch. Distributed by [MIT License](https://svelte-on-rails-docs-51acfa.gitlab.io/license.html)
@@ -141,7 +141,7 @@ module SvelteOnRails
141
141
  end
142
142
 
143
143
  def haml_and_convert
144
- return unless options[:full] || options[:haml]
144
+ return unless options[:haml]
145
145
 
146
146
  puts '-' * 80
147
147
  puts ' ▶︎▶︎▶︎ INSTALLING Haml and converting existing .erb files to .haml'
@@ -8,32 +8,33 @@ module SvelteOnRails
8
8
  # end
9
9
 
10
10
  # Returns a hash of attributes, methods, and associations formatted for Svelte components
11
- def to_svelte(*attributes)
11
+ def to_svelte(*attributes, **associations)
12
12
  @to_svelte ||= begin
13
13
 
14
- cl = SvelteOnRails::Lib::SvelteAttributes
15
- cl.new.calculate_instance(self, attributes)
14
+ cl = SvelteOnRails::Lib::SvelteAttributes
15
+ r = cl.new.calculate_instance(self, attributes, associations)
16
+ r.first.merge({self.class.to_s.underscore => r[1]})
16
17
 
17
- end
18
+ end
18
19
  end
19
20
 
20
21
  end
21
22
 
22
23
  module ActiveRecordClassExtensions
23
- def to_svelte(*attributes)
24
+ def to_svelte(*attributes, **associations)
24
25
  @to_svelte ||= begin
25
- cl = SvelteOnRails::Lib::SvelteAttributes
26
- cl.new.calculate_class(self, attributes)
27
- end
26
+ cl = SvelteOnRails::Lib::SvelteAttributes
27
+ cl.new.calculate_class(self, attributes, associations)
28
+ end
28
29
  end
29
30
  end
30
31
 
31
32
  module ActiveRecordRelationExtensions
32
- def to_svelte(*attributes)
33
+ def to_svelte(*attributes, **associations)
33
34
  @to_svelte ||= begin
34
- cl = SvelteOnRails::Lib::SvelteAttributes
35
- cl.new.calculate_relation(self, attributes)
36
- end
35
+ cl = SvelteOnRails::Lib::SvelteAttributes
36
+ cl.new.calculate_relation(self, attributes, associations)
37
+ end
37
38
  end
38
39
  end
39
40
 
@@ -6,7 +6,7 @@ module SvelteOnRails
6
6
  @labels = {}
7
7
  end
8
8
 
9
- def calculate_instance(record, attributes, call_stack: 0, offset: nil, limit: nil)
9
+ def calculate_instance(record, attributes, associations, call_stack: 0, offset: nil, limit: nil)
10
10
 
11
11
  next_stack = call_stack + 1
12
12
 
@@ -26,10 +26,10 @@ module SvelteOnRails
26
26
  record
27
27
  end
28
28
 
29
- set_labels(record.first, attributes)
29
+ # set_labels(record.first, attributes)
30
30
 
31
31
  values = recs2.map do |rec|
32
- calculate_instance(rec, attributes, call_stack: next_stack)
32
+ calculate_instance(rec, attributes, associations, call_stack: next_stack)
33
33
  end
34
34
 
35
35
  else
@@ -38,81 +38,82 @@ module SvelteOnRails
38
38
 
39
39
  values = {}
40
40
 
41
- set_labels(record, attributes)
41
+ set_labels(record, attributes, associations)
42
42
 
43
43
  attributes.each do |attr|
44
+ raise 'Invalid attribute' unless [Symbol, String].include?(attr.class)
45
+ raise "[svelte-on-rails:to_svelte] #{record.class} does not respond to: #{attr}" unless record.respond_to?(attr)
46
+ _key = attr.to_s
44
47
 
45
- if attr.is_a? Hash
46
-
47
- # we have associations
48
- attr.each do |key, value|
49
- next if ['offset', 'limit'].include?(key.to_s)
50
- raise "[svelte-on-rails:to_svelte] #{record.class} does not respond to: #{key}" unless record.respond_to?(key)
51
- _offset, _limit, _value = extract_limit(value)
52
-
53
- _key = key.to_s
54
-
55
- # inspect association and set_labels
56
-
57
- reflect = record.class.reflect_on_association(_key)
58
- raise "invalid association: #{_key}" unless reflect
59
- set_labels(reflect, value)
60
-
61
- # values
48
+ values[_key] = record.send(_key)
49
+ end
62
50
 
63
- recs = record.send(_key)
64
- if recs.present?
65
- if recs.respond_to?(:each)
66
- values[_key] = calculate_instance(
67
- recs,
68
- value,
69
- call_stack: next_stack,
70
- offset: _offset,
71
- limit: _limit
72
- )
73
- else
74
- values[_key] = calculate_instance(
75
- recs,
76
- value,
77
- call_stack: next_stack
78
- )
79
- end
51
+ associations.each do |key, val|
52
+
53
+ # we have associations
54
+ val.each do |_key|
55
+ next if ['offset', 'limit'].include?(_key.to_s)
56
+ raise "[svelte-on-rails:to_svelte] #{record.class} does not respond to: #{key}" unless record.respond_to?(key)
57
+ _offset, _limit, _value = extract_limit(val)
58
+
59
+ _key = key.to_s
60
+
61
+ # inspect association and set_labels
62
+
63
+ reflect = record.class.reflect_on_association(_key)
64
+ raise "invalid association: #{_key}" unless reflect
65
+ set_labels(reflect, val)
66
+
67
+ # values
68
+
69
+ recs = record.send(_key)
70
+ if recs.present?
71
+ if recs.respond_to?(:each)
72
+ values[_key] = calculate_instance(
73
+ recs,
74
+ val.reject{|v|v.is_a?(Hash)},
75
+ {},
76
+ call_stack: next_stack,
77
+ offset: _offset,
78
+ limit: _limit
79
+ )
80
+ else
81
+ values[_key] = calculate_instance(
82
+ recs,
83
+ val,
84
+ {},
85
+ call_stack: next_stack
86
+ )
80
87
  end
81
88
  end
82
-
83
- else
84
- # we have attributes
85
- raise 'Invalid attribute' unless [Symbol, String].include?(attr.class)
86
- raise "[svelte-on-rails:to_svelte] #{record.class} does not respond to: #{attr}" unless record.respond_to?(attr)
87
- _key = attr.to_s
88
-
89
- values[_key] = record.send(_key)
90
89
  end
91
-
92
90
  end
93
91
  end
94
92
 
95
93
  if call_stack >= 1
96
94
  values
97
95
  else
98
- { 'values' => values }.merge(@labels)
96
+ [
97
+ @labels,
98
+ values
99
+ ]
99
100
  end
100
101
 
101
102
  end
102
103
 
103
- def calculate_class(model, attributes, call_stack: 0)
104
+ def calculate_class(model, attributes, associations, call_stack: 0)
104
105
 
105
106
  next_stack = call_stack + 1
106
107
 
107
- set_labels(model, attributes)
108
+ set_labels(model, attributes, associations)
108
109
 
109
- hash = attributes.find { |element| element.is_a?(Hash) } || {}
110
- hash.each do |key, value|
110
+ associations.each do |key, value|
111
111
  reflect = model.reflect_on_association(key.to_s)
112
112
  if reflect
113
113
  calculate_class(
114
114
  reflect,
115
115
  value,
116
+ {},
116
117
  call_stack: next_stack
117
118
  )
118
119
  end
@@ -123,14 +124,16 @@ module SvelteOnRails
123
124
  end
124
125
  end
125
126
 
126
- def calculate_relation(relation, attributes, call_stack: 0)
127
+ def calculate_relation(relation, attributes, associations)
127
128
  set_labels(relation.klass, attributes)
128
- values = relation.map do |rec|
129
- calculate_instance(rec, attributes)['values']
129
+ r = relation.map do |rec|
130
+ calculate_instance(rec, attributes, associations, call_stack: 1)
130
131
  end
131
- {
132
- 'values' => values
133
- }.merge(@labels)
132
+
133
+ @labels.merge({
134
+ relation.klass.to_s.underscore.pluralize => r
135
+ })
136
+
134
137
  end
135
138
 
136
139
  private
@@ -164,11 +167,9 @@ module SvelteOnRails
164
167
  ]
165
168
  end
166
169
 
167
- def set_labels(record, keys)
170
+ def set_labels(record, keys, assoc = {})
168
171
 
169
- first_hash = keys.find { |element| element.is_a?(Hash) }
170
- _keys = keys.reject { |element| element.is_a?(Hash) }
171
- _keys += first_hash.keys if first_hash
172
+ _keys = keys.reject { |element| element.is_a?(Hash) } + assoc.keys
172
173
 
173
174
  _keys.each do |attr|
174
175
  unless attr.respond_to?(:each)
@@ -3,19 +3,23 @@ module SvelteOnRails
3
3
 
4
4
  module Lib
5
5
  class ViewHelperSupport
6
- attr_reader :filename, :helper_options, :html_options, :request, :conf
6
+ attr_reader :filename, :options, :html_options, :request, :conf
7
7
 
8
- def initialize(file, args, request, caching = false)
8
+ def initialize(file, props, html_options, options, request, caching = false)
9
9
 
10
10
  @start_time = Time.now
11
11
  @conf = SvelteOnRails::Configuration.instance
12
12
  utils = SvelteOnRails::Lib::Utils
13
13
  utils.validate_filename(file) if @conf.watch_changes?
14
14
  @filename = (file.match(/\.svelte$/) ? file[0..-8] : file)
15
- @args_checksum = Zlib.crc32(args.to_json).to_s(36)
16
- @helper_options, @html_options, @svelte_props = split_props(
17
- args,
18
- %i[class id style],
15
+ @args_checksum = [
16
+ Zlib.crc32(props.to_json).to_s(36),
17
+ Zlib.crc32(options.to_json).to_s(36)
18
+ ].join('-')
19
+ @svelte_props = props ||= {}
20
+ @options, @html_options = prepare_options(
21
+ options,
22
+ html_options,
19
23
  %i[ssr hydrate debug cache_key expires_in]
20
24
  )
21
25
  @request = request
@@ -32,8 +36,8 @@ module SvelteOnRails
32
36
  if caching
33
37
  raise '[svelte-on-rails] Caching required but Redis is not defined' unless defined?(Redis)
34
38
  else
35
- raise '[svelte-on-rails] :expires_in is not allowed for this helper' if @helper_options.key?(:expires_in)
36
- raise '[svelte-on-rails] :cache_key is not allowed for this helper' if @helper_options.key?(:cache_key)
39
+ raise '[svelte-on-rails] :expires_in is not allowed for this helper' if @options.key?(:expires_in)
40
+ raise '[svelte-on-rails] :cache_key is not allowed for this helper' if @options.key?(:cache_key)
37
41
  return
38
42
  end
39
43
 
@@ -42,11 +46,11 @@ module SvelteOnRails
42
46
  end
43
47
 
44
48
  def debug?
45
- @helper_options[:debug]
49
+ @options[:debug]
46
50
  end
47
51
 
48
52
  def redis_expiration_seconds
49
- (conf.redis_cache_store[:expires_in] || @helper_options[:expires_in] || 1.hour).to_i
53
+ (conf.redis_cache_store[:expires_in] || @options[:expires_in] || 1.hour).to_i
50
54
  end
51
55
 
52
56
  def elapsed_milliseconds
@@ -89,8 +93,8 @@ module SvelteOnRails
89
93
  end
90
94
 
91
95
  def custom_cache_key
92
- if @helper_options[:cache_key]
93
- k2 = (@helper_options[:cache_key])
96
+ if @options[:cache_key]
97
+ k2 = (@options[:cache_key])
94
98
  keys = k2.is_a?(Array) ? k2 : [k2]
95
99
  keys.map do |k|
96
100
  if k.is_a?(ActiveRecord::Base)
@@ -109,13 +113,13 @@ module SvelteOnRails
109
113
  private
110
114
 
111
115
  def determine_ssr
112
- _ssr = if @helper_options.key?(:ssr)
116
+ _ssr = if @options.key?(:ssr)
113
117
  if @conf.watch_changes?
114
- unless [true, false, :auto].include?(@helper_options[:ssr])
118
+ unless [true, false, :auto].include?(@options[:ssr])
115
119
  raise "Only true, false, or :auto are allowed for the argument #ssr"
116
120
  end
117
121
  end
118
- @helper_options[:ssr]
122
+ @options[:ssr]
119
123
  else
120
124
  @conf.ssr
121
125
  end
@@ -147,32 +151,36 @@ module SvelteOnRails
147
151
 
148
152
  end
149
153
 
150
- def split_props(args, html_options, helper_options)
151
- prp = {}
152
- hlp_opts = {}
153
- ht_opts = {
154
- data: {
155
- svelte_component: "/#{conf.components_folder + filename}",
156
- controller: 'svelte-on-rails',
154
+ def prepare_options(options, html_options, available_options)
155
+
156
+ # html options
157
+
158
+ ht_opts = html_options.dup.deep_merge(
159
+ {
160
+ data: {
161
+ svelte_component: "/#{conf.components_folder + filename}",
162
+ controller: 'svelte-on-rails'
163
+ }
157
164
  }
158
- }
165
+ )
166
+ ht_opts[:class] = "#{ht_opts[:class]} svelte-component".strip
167
+ ht_opts[:data][:props] = @svelte_props.to_json
168
+ ht_opts[:data][:svelte_status] = 'do-not-hydrate-me' if options[:hydrate] == false
159
169
 
160
- args.each do |k, v|
170
+ # render options
171
+
172
+ opts = {}
173
+ options.each do |k, v|
161
174
  _k = k.to_sym
162
- if helper_options.include?(_k)
163
- hlp_opts[_k] = v
164
- elsif html_options.include?(_k)
165
- ht_opts[_k] = v
175
+ if available_options.include?(_k)
176
+ opts[_k] = v
166
177
  else
167
- prp[_k] = v
178
+ raise("Unknown option: #{k}")
168
179
  end
169
180
  end
170
181
 
171
- ht_opts[:class] = "#{ht_opts[:class]} svelte-component".strip
172
- ht_opts[:data][:props] = prp.to_json
173
- ht_opts[:data][:svelte_status] = 'do-not-hydrate-me' if hlp_opts[:hydrate] == false
174
-
175
- [hlp_opts, ht_opts, prp]
182
+
183
+ [opts, ht_opts]
176
184
  end
177
185
 
178
186
  end
@@ -2,11 +2,11 @@
2
2
  module SvelteOnRails
3
3
  module ViewHelpers
4
4
 
5
- def svelte_component(filename, props = {})
5
+ def svelte_component(path, props = {}, html: {}, options: {})
6
6
 
7
- support = SvelteOnRails::Lib::ViewHelperSupport.new(filename, props, request, false)
7
+ support = SvelteOnRails::Lib::ViewHelperSupport.new(path, props, html, options, request, false)
8
8
 
9
- support.debug_log("Rendering component: #{filename}")
9
+ support.debug_log("Rendering component: #{path}")
10
10
  log_message = '?'
11
11
 
12
12
  log_message = "Rendered #{support.filename}.svelte #{'as empty element that will be mounted on the client side' unless support.ssr?}"
@@ -18,9 +18,9 @@ module SvelteOnRails
18
18
 
19
19
  end
20
20
 
21
- def cached_svelte_component(filename, props = {})
21
+ def cached_svelte_component(path, props = {}, html: {}, options: {})
22
22
 
23
- support = SvelteOnRails::Lib::ViewHelperSupport.new(filename, props, request, true)
23
+ support = SvelteOnRails::Lib::ViewHelperSupport.new(path, props, html, options, request, true)
24
24
 
25
25
  log_message = '?'
26
26
  redis = support.conf.redis_instance
@@ -23,7 +23,7 @@
23
23
  <h3>Always rendered server side</h3>
24
24
  <div class="ssr-only">
25
25
  <% components.each do |component| %>
26
- <%= svelte_component(component, ssr: true, hydrate: false, title: component) %>
26
+ <%= svelte_component(component, {title: component}, options: {ssr: true, hydrate: false}) %>
27
27
  <% end %>
28
28
  </div>
29
29
 
@@ -32,6 +32,6 @@
32
32
  <p>Here you will see a unpleasant «blink» on the initial request.</p>
33
33
  <div class="client-only">
34
34
  <% components.each do |component| %>
35
- <%= svelte_component(component, ssr: false, hydrate: true, title: component) %>
35
+ <%= svelte_component(component, {title: component}, options: {ssr: false, hydrate: true}) %>
36
36
  <% end %>
37
37
  </div>
@@ -6,4 +6,4 @@
6
6
 
7
7
  <h1>Svelte is here</h1>
8
8
 
9
- <%= svelte_component "SvelteOnRailsHelloWorld", items: ['attributes', 'are', 'parsed'] %>
9
+ <%= svelte_component "SvelteOnRailsHelloWorld", { items: ['attributes', 'are', 'parsed'] } %>
@@ -20,7 +20,7 @@
20
20
  <h3>Rendered server side only on initial request</h3>
21
21
  <div class="ssr-auto">
22
22
  <% components.each do |component| %>
23
- <%= svelte_component(component, title: component) %>
23
+ <%= svelte_component(component, { title: component }) %>
24
24
  <% end %>
25
25
  </div>
26
26
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: svelte-on-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.1
4
+ version: 7.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Sedlmair
@@ -78,13 +78,13 @@ files:
78
78
  - templates/config_base/app/frontend/ssr/ssr.js
79
79
  - templates/config_base/config/svelte_on_rails.yml
80
80
  - templates/config_base/vite-ssr.config.ts
81
- homepage: https://gitlab.com/sedl/svelte-on-rails
81
+ homepage: https://svelte-on-rails-docs-51acfa.gitlab.io/
82
82
  licenses:
83
83
  - MIT
84
84
  metadata:
85
- homepage_uri: https://gitlab.com/sedl/svelte-on-rails
86
- source_code_uri: https://gitlab.com/sedl/svelte-on-rails
87
- changelog_uri: https://gitlab.com/sedl/svelte-on-rails
85
+ homepage_uri: https://svelte-on-rails-docs-51acfa.gitlab.io/
86
+ source_code_uri: https://svelte-on-rails-docs-51acfa.gitlab.io/
87
+ changelog_uri: https://svelte-on-rails-docs-51acfa.gitlab.io/
88
88
  post_install: ruby -r svelte_on_rails/install -e 'SvelteOnRails::Install.run'
89
89
  rdoc_options: []
90
90
  require_paths: