svelte-on-rails 4.1.1 → 5.1.0

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: f1113ad80ad88261fdf387b8d033006e10cedcbb0bd4e941a5a6fd8d7c338e2e
4
- data.tar.gz: de7a37961949e4ff3f99de2da8818e159b3afc552dd530f1373607101c2a50db
3
+ metadata.gz: 0e2782073f696f72736004ae21d57a71e4429fb878f915954e36628e16904de9
4
+ data.tar.gz: 2d041a60e7908ce20ab64f490ccebe3d3c673882986c83a64981aaf1e9931768
5
5
  SHA512:
6
- metadata.gz: e173b7d27296c0a51f7ea6d35632fab227e34964353087354fcd10634efc004959140da5724094b486dbb0a009e10af7a65e809d152b5621ac82911d5efc5198
7
- data.tar.gz: 0d8f6bc08236e2752c81c7f1353f36c41c67ea9e4937410658ead038351b1ff0a323992af634b8b084e36d017facb6775cc41b0efa3cabd2989f5cd47620d81d
6
+ metadata.gz: 5d1fef990a79dfa782b0101f33ed64f3631d126ea6ef16dfde8d21d240653d8d938a87689955af288b862362216fbb760db61e79d709c512b242d0f86ad823d1
7
+ data.tar.gz: 430b9cf7ab45fb7e0ab629b4de95bd262e8b02bd68a58793dcd614832dfd2bb04d63f52d20ddc6639dc951d8ae132188dee755dadab1042f98a0268dcf4bab63
data/README.md CHANGED
@@ -12,34 +12,35 @@ Svelte.
12
12
 
13
13
  Vite.
14
14
 
15
- Hotwired.
16
-
17
- # Why Svelte?
18
-
19
- On every app there are parts where you want it to shine. This is where Svelte comes in.
20
-
21
- - Works perfectly with Hotwired/Turbo
22
- - Perfect addition to Stimulus
23
- - Easy to learn
24
- - Powerful
25
- - Super fast
26
- - **Compared to Stimulus**
27
- - No more writing double logic of the initial HTML state
28
- - One Logic in one file!
29
- - Render logic and script logic is in one file and, maybe, some css too.
30
- - Stimulus is great for rails views with some javascript, but, complex parts: You will love svelte.
31
- - Improved Backend Response Time: Certain HTML components can be rendered directly by frontend JavaScript.
15
+ # Why Choose Svelte for Integration?
16
+
17
+ Svelte delivers the robust frontend experience missing from DHH’s
18
+ vision while aligning seamlessly with Rails’ full-stack philosophy.
19
+
20
+ - **Seamless Integration**
21
+ - Works flawlessly with Hotwired/Turbo
22
+ - Complements Hotwired perfectly
23
+ - **Developer-Friendly**
24
+ - Easy to learn
25
+ - Intuitive and powerful
26
+ - Lightning-fast performance
27
+ - **Compared to Single Page Apps (SPAs)**
28
+ - Full-stack development delivers maximum value:
29
+ - Unified testing from database to frontend
30
+ - Single-source system delivery
31
+ - For the most HTML Hotwired is enough
32
+ - **Compared to Hotwired**
33
+ - Stimulus is not a tool for writing frontend-apps
34
+ - Svelte eliminates redundant HTML state logic
35
+ - Consolidates component logic into a single file
36
+ - Offloads rendering of certain HTML components to frontend JavaScript, reducing server load
32
37
  - **Compared to React**
33
- - No more shadow dom and all those packages that are supposed to improve performance (e.g. useCallback...)
34
- - Slimmer packages
35
- - Easier to learn
36
- - Faster
38
+ - Watch Rich Harris’ [Rethinking Reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity) (3:50–6:40) for a compelling comparison
39
+ - No shadow DOM or performance-heavy packages (e.g., useCallback)
40
+ - Leaner, slimmer packages
41
+ - Easier to learn and faster performance
37
42
 
38
- Have a look at this entertaining video [rethinking reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity)
39
- by Rich Harris, especially from 3:50 to 6:40 and his comparison to react.
40
-
41
- This all fits in perfectly with the Rails way of minimalist javascript,
42
- but: Where needed, we want full power.
43
+ Svelte empowers Rails’ full-stack vision with modern, streamlined frontend integration.
43
44
 
44
45
  # Features
45
46
 
@@ -314,13 +315,13 @@ When using the `cached_svelte_component` helper you must have the `redis` gem in
314
315
 
315
316
  This caches on a key like `svelte-on-rails:development:SvelteOnRailsHelloWorld.svelte-1xq5tnu-User1:fscyhz-18bm76a` which includes:
316
317
 
317
- - gem name and environment
318
+ - Namespace, if configured, otherwise the default is gem-name and environment
318
319
  - component filename
319
- - hash of the file-path
320
+ - checksum of the file-path
320
321
  - `cache_key` if given as argument to the view-helper
321
322
  - can be a array of active-record objects or strings or a single element instead of a array
322
- - hash of the last modification timestamp of the component (only when `watch_changes` is set to true)
323
- - hash of the given attributes
323
+ - checksum of the last modification timestamp of the component (only when `watch_changes` is set to true)
324
+ - checksum of the given attributes
324
325
 
325
326
  **Configuration**
326
327
 
@@ -339,12 +340,187 @@ redis_cache_store:
339
340
  expires_in: 2.hours
340
341
  ```
341
342
 
342
- on the svelte-on-rails config file.
343
+ on the svelte-on-rails config file or pass the `expires_in` as argument to the view helper.
343
344
 
344
345
  **Check if it works**
345
346
 
346
347
  Pass `debug: true` to the helper and you will see on the logs how your configuration works.
347
348
 
349
+ ## ActionCable / TurboStream
350
+
351
+ There are two ways that the server can talk to the client over Websocket:
352
+
353
+ - ActionCable transmits directly to the frontends javascript
354
+ - TurboStreams is a wrapper around ActionCable
355
+ - You always need a html part for communication
356
+
357
+ **Configs**
358
+
359
+ 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).
360
+ From there, you just need:
361
+
362
+ ```yaml
363
+ turbo_stream:
364
+ target_html_id: 'svelte-on-rails-stream-actions-box'
365
+ channel: 'public'
366
+ ```
367
+
368
+ ## ActionCable!
369
+
370
+ ActionCable is the more basic library behind `TurboStream` and it is a second Option to call Javascript Actions from the server.
371
+
372
+ **Setup**
373
+
374
+ Add `app/channels/svelte_on_rails_channel.rb`
375
+
376
+ ```ruby
377
+ class SvelteOnRailsChannel < ApplicationCable::Channel
378
+ def subscribed
379
+ stream_from "svelte_on_rails_channel"
380
+ end
381
+
382
+ def unsubscribed
383
+ # Any cleanup needed when channel is unsubscribed
384
+ end
385
+ end
386
+ ```
387
+
388
+ run
389
+
390
+ ```shell
391
+ npm i @rails/actioncable
392
+ ```
393
+
394
+ Add to `application.js`
395
+
396
+ ```javascript
397
+ import { createConsumer } from "@rails/actioncable"
398
+ import {dispatchSvelteStreamEvent, actionCableDebugLog} from '@csedl/svelte-on-rails'
399
+
400
+ const consumer = createConsumer()
401
+
402
+ consumer.subscriptions.create("SvelteOnRailsChannel", {
403
+ connected() {
404
+ actionCableDebugLog("Connected to SvelteOnRailsChannel")
405
+ },
406
+ disconnected() {
407
+ actionCableDebugLog("Disconnected from SvelteOnRailsChannel")
408
+ },
409
+ received(data) {
410
+ actionCableDebugLog("Received:", data)
411
+ dispatchSvelteStreamEvent(data)
412
+ }
413
+ })
414
+ ```
415
+
416
+ By setting `debug = true` [node package](https://www.npmjs.com/package/@csedl/svelte-on-rails) you see what it is doing.
417
+
418
+ **WhatdispatchSvelteStreamEvent does**
419
+
420
+ - Without the attribute `component` given,
421
+ it searches for all elements with the class `svelte-component` and fires the event `channel-action`
422
+ - When `selector` is given, it searches for all matching elements within each component.
423
+ - The event can be overriden by the argument `event`
424
+
425
+ **Usage**
426
+
427
+ Now you can dispatch events on the component by:
428
+
429
+ ```ruby
430
+ ActionCable.server.broadcast("svelte_on_rails_channel",
431
+ {
432
+ eventDetail: { message: "Sent by ActionCable: äöü🤣🌴🌍漢字" },
433
+ component: '/javascript/components/ReceiveFromChannel',
434
+ }
435
+ )
436
+ ```
437
+
438
+ # Turbo::StreamsChannel.send!
439
+
440
+ Few setup is needed for that:
441
+
442
+ **Setup**
443
+
444
+ 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:
445
+
446
+ ```shell
447
+ bundle add turbo-rails
448
+ rails turbo:install
449
+ ```
450
+
451
+ make sure that `import "@hotwired/turbo-rails"` is on your application.js and set the view helper
452
+ `<%= turbo_stream_from 'public' %>` to your view.
453
+
454
+ If a channel (e.g.: `public`) is active and you have an HTML element with a HTML-ID (e.g.: `svelte-on-rails-stream-actions-box`),
455
+ you can test it by:
456
+
457
+ ```ruby
458
+ Turbo::StreamsChannel.send(
459
+ "broadcast_append_to",
460
+ 'public',
461
+ target: 'svelte-on-rails-stream-actions-box',
462
+ content: "<div>Turbo-Streams are working!</div>"
463
+ )
464
+ ```
465
+
466
+ When this works you are good to go.
467
+
468
+ **Minimal Usage Example**
469
+
470
+ ```sveltehtml
471
+ <script>
472
+ import {addComponentStreamListener} from '@csedl/svelte-on-rails/src/componentStreamListener'
473
+
474
+ function handleCableEvent() {
475
+ console.log('Event received by Turbo Stream')
476
+ }
477
+ </script>
478
+ <!--on any element:-->
479
+ <h1 use:addComponentStreamListener={handleCableEvent}>Test TurboStreams Channel</h1>
480
+ ```
481
+
482
+ And call this by:
483
+
484
+ ```ruby
485
+ SvelteOnRails::TurboStream.new.dispatch_event
486
+ ```
487
+ **What it does**
488
+
489
+ - The `addComponentStreamListener` adds the eventListener `stream-action` on the wrapping Element
490
+ - The «wrapping Element» is the Element from the view helper `svelte_component` with the class `svelte-component`.
491
+ - A Stimulus controller is pushed to all subscribers of the configured channel.
492
+ - You can override the channel name by passing `channel` to the method.
493
+
494
+ **Usage Example, more detailed**
495
+
496
+ If you want to fire a event on a specific element within the component, you do not need `addComponentStreamListener`.
497
+ Just do something like:
498
+
499
+ ```sveltehtml
500
+ <script>
501
+ function logStreamMessage(event) {
502
+ console.log(event.detail.message)
503
+ }
504
+ </script>
505
+ <button class="counter-button" onclick="{logStreamMessage}">Show Contents</button>
506
+ ```
507
+
508
+ within the svelte component.
509
+
510
+ Then, call it by:
511
+
512
+ ```ruby
513
+ SvelteOnRails::TurboStream.new.dispatch_event(
514
+ channel: 'my-custom-stream',
515
+ component: '/javascript/components/TurboStreamsChannel',
516
+ selector: '.counter-button',
517
+ event: 'click',
518
+ event_detail: { message: "Greetings from Server: äöü🤣🌴🌍漢字" }
519
+ )
520
+ ```
521
+
522
+
523
+
348
524
  ## More rake tasks
349
525
 
350
526
  This tasks are more for testing/playground purposes
@@ -398,10 +574,14 @@ This will cause the installer, to install the npm package from a local path inst
398
574
 
399
575
  Then run the tests and start contributing.
400
576
 
577
+ **RUN THE FIRST TEST (!)**: Testing is complex here because of the design based on the installer test.
578
+ On Problems, i always run the `Installer > destroy and create rails app > FIRST TEST > [...] check if javascript works`. When this passes,
579
+ all the others passing mostly.
580
+
401
581
  Tests are based on the included templates, like the `hello world template` and on the installer.
402
582
 
403
583
  Here I learned how to write tests! Testing installers is heavy, you will read about it in the
404
- test_helpers. But I have done my best to give all the code a clear structure.
584
+ test_helpers. But I have done my best to give all the code a clear structure.
405
585
  I hope you like it, and improvements are welcome.
406
586
 
407
587
  **Installer tests** starting with completely destroy the rails app within the `generated_test_app_path`,
@@ -9,6 +9,7 @@ module SvelteOnRails
9
9
  class_option :vite, type: :boolean, default: false, desc: "Use Vite"
10
10
  class_option :haml, type: :boolean, default: false, desc: "Use Haml"
11
11
  class_option :turbo, type: :boolean, default: false, desc: "Use @hotwired/turbo-rails"
12
+ class_option :turbo_streams, type: :boolean, default: false, desc: "Turbo::StreamsChannel"
12
13
  class_option :svelte, type: :boolean, default: false, desc: "Install Svelte"
13
14
  class_option :pug, type: :boolean, default: false, desc: "Install Pug"
14
15
  class_option :hello_world, type: :boolean, default: false, desc: "Create Hello World component"
@@ -88,6 +89,20 @@ module SvelteOnRails
88
89
  js_i = SvelteOnRails::Installer::Javascript
89
90
  js_i.append_import_statement(application_js_path, tr_pkg, "import '#{tr_pkg}';")
90
91
 
92
+ return unless options[:turbo_streams] || options[:full]
93
+
94
+ puts '-' * 80
95
+ puts ' ▶︎▶︎▶︎ INSTALLING Turbo::StreamsChannel'
96
+ puts '-' * 80
97
+
98
+ gem_uts = SvelteOnRails::GemUtils
99
+ puts '• installing turbo-rails ...'
100
+ gem_uts.install_gem('turbo-rails')
101
+ puts '• installed turbo-rails!'
102
+ utils_i = SvelteOnRails::Installer::Utils
103
+ puts '• running turbo:install ...'
104
+ utils_i.run_command('bundle exec rails turbo:install', success_message: 'You must import')
105
+ puts '• turbo:install finished!'
91
106
  end
92
107
 
93
108
  def svelte
@@ -112,6 +127,12 @@ module SvelteOnRails
112
127
  puts '-' * 80
113
128
 
114
129
  hw_i = SvelteOnRails::Installer::HelloWorld
130
+
131
+ utils_i = SvelteOnRails::Installer::Utils
132
+ utils_i.add_route(" get \"svelte_on_rails_hello_world/turbo_streams_channel\"", app_root: Rails.root)
133
+ utils_i.add_route(" get \"svelte_on_rails_hello_world/turbo_stream_action\"", app_root: Rails.root)
134
+ npm_i = SvelteOnRails::Installer::Npm
135
+ npm_i.install_or_update_package('axios')
115
136
  @hello_world_path = hw_i.install_hello_world(['rails_vite_hello_world'], app_root: nil, force: true, silent: true)
116
137
  end
117
138
 
@@ -160,6 +181,10 @@ module SvelteOnRails
160
181
  valid_display = valid_options.map { |opt| opt.gsub(/^--/, '') }.join(', ')
161
182
  raise Thor::Error, "Unknown options: #{unknown_options.join(', ')}. Valid options are: #{valid_display}\nNothing done."
162
183
  end
184
+
185
+ if options[:turbo_streams] && !(options[:full] || options[:turbo])
186
+ raise Thor::Error, "--turbo-streams option cannot be used without --turbo"
187
+ end
163
188
  end
164
189
 
165
190
  def application_js_path
@@ -1,9 +1,13 @@
1
1
  require "svelte_on_rails/configuration"
2
2
  require "svelte_on_rails/view_helpers"
3
+ require "svelte_on_rails/turbo_stream"
4
+ require "svelte_on_rails/action_cable"
5
+ require "svelte_on_rails/railtie" if defined?(Rails)
6
+
3
7
  require "svelte_on_rails/renderer/renderer"
8
+
4
9
  require "svelte_on_rails/lib/utils"
5
10
  require "svelte_on_rails/lib/view_helper_support"
6
- require "svelte_on_rails/railtie" if defined?(Rails)
7
11
 
8
12
  # installer
9
13
  require 'svelte_on_rails/installer/utils'
@@ -0,0 +1,42 @@
1
+ module SvelteOnRails
2
+ class ActionCable
3
+ def dispatch_event(event: 'stream-action', event_detail: nil, selector: nil, component: nil, channel: "svelte_on_rails_channel")
4
+
5
+ if event != 'stream-action' && !selector
6
+ raise "Another event name than the default one is only possible together with a selector"
7
+ end
8
+
9
+ args = {
10
+ eventDetail: event_detail,
11
+ component: component,
12
+ event: event,
13
+ selector: selector
14
+ }
15
+
16
+ #args_enc = Base64.strict_encode64(args.to_json)
17
+
18
+ ActionCable.server.broadcast(channel, args)
19
+
20
+ end
21
+
22
+ private
23
+
24
+ def configs
25
+ @configs ||= begin
26
+
27
+ conf = SvelteOnRails::Configuration.instance
28
+ unless conf.configs[:turbo_stream]
29
+ raise '[svelte-on-rails] missing configuration: :turbo_stream'
30
+ end
31
+ unless conf.configs[:turbo_stream]['target_html_id']
32
+ raise '[svelte-on-rails] missing configuration: turbo_stream/target_html_id'
33
+ end
34
+ unless conf.configs[:turbo_stream]['channel']
35
+ raise '[svelte-on-rails] missing configuration: turbo_stream/channel'
36
+ end
37
+
38
+ conf.configs[:turbo_stream]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -26,7 +26,7 @@ module SvelteOnRails
26
26
  if stderr.present?
27
27
  raise stderr
28
28
  else
29
- puts "Installed #{gem_name} version #{check_gem_version(gem_name)}"
29
+ puts "Successfully installed #{gem_name} version #{check_gem_version(gem_name)}"
30
30
  end
31
31
  end
32
32
  end
@@ -37,6 +37,7 @@ module SvelteOnRails
37
37
 
38
38
  end
39
39
 
40
+
40
41
  def self.remove_hello_world(templates = ['rails_vite_hello_world'], app_root: nil, ask: true)
41
42
 
42
43
  utils = SvelteOnRails::Installer::Utils
@@ -133,15 +133,26 @@ module SvelteOnRails
133
133
  end
134
134
  end
135
135
 
136
- def self.run_command(command)
136
+ def self.run_command(command, success_message: false)
137
137
 
138
138
  Dir.chdir(Rails.root) do
139
- stdout, stderr, status = Open3.capture3(command)
140
- if stderr.present?
141
- puts "Error running command «#{command}»:"
142
- raise stderr
143
- else
144
- puts "#{command} => Success"
139
+ Bundler.with_unbundled_env do
140
+ `pwd`
141
+ stdout, stderr, status = Open3.capture3(command)
142
+ err_msg = if stderr.present?
143
+ stderr.strip
144
+ elsif success_message && !stdout.match(/#{success_message}/)
145
+ "missing string «#{success_message}» on result:\n\n+++#{stdout}\n+++"
146
+ else
147
+ end
148
+
149
+ if err_msg
150
+ puts "Error running command «#{command}»:"
151
+ raise err_msg
152
+ else
153
+ puts "#{command} => Success"
154
+ end
155
+
145
156
  end
146
157
  end
147
158
 
@@ -179,11 +190,12 @@ module SvelteOnRails
179
190
 
180
191
  if existing.present? && ask_for_overwrite
181
192
  begin
182
- puts "#{'File'.pluralize(existing.length)} already exists:\n#{existing.map { |p| p[1] }.join("\n")}.\nOverwrite? (y/n)"
193
+ puts "#{'File'.pluralize(existing.length)} already exists:\n#{existing.map { |p| p[1] }.join("\n")}.\nOverwrite? (y/n)
194
+ "
183
195
  continue = STDIN.gets.chomp.downcase[0]
184
196
  end until ['y', 'n'].include?(continue)
185
197
  if continue == 'n'
186
- puts "Skipping write #{'template'.pluralize(templates.length)}."
198
+ puts " Skipping write #{'template'.pluralize(templates.length)}."
187
199
  return
188
200
  end
189
201
  end
@@ -2,18 +2,25 @@ import {loadComponentModule, readPropsFromStdin} from './utils.js';
2
2
 
3
3
  (async () => {
4
4
 
5
+ console.log(`[svelte-on-rails:debug] awaiting load component => «${process.argv[2]}»`)
6
+
5
7
  const MyComponent = await loadComponentModule(process.argv[2]);
8
+ console.log(`[svelte-on-rails:debug] component read: «${MyComponent}}»`)
9
+
6
10
  const props = await readPropsFromStdin();
7
- const payload = {out: ''};
11
+ console.log(`[svelte-on-rails:debug] props read: «${props}»`)
12
+
13
+ const payload = {out: []};
8
14
 
9
15
  try {
10
16
  MyComponent(payload, props); // Writes directly to payload.out
17
+ console.log(`[svelte-on-rails:debug] written to payload`)
11
18
  } catch (error) {
12
- console.error('Error rendering component:', error);
19
+ console.error('[svelte-on-rails:debug] Error rendering component:', error);
13
20
  process.exit(1);
14
21
  }
15
22
 
16
- const res = {status: 'SUCCESS', html: payload.out};
17
- console.log(JSON.stringify(res));
23
+ const res = {status: 'SUCCESS', html: payload.out.join('')};
24
+ console.log('[svelte-on-rails:successful-json-response]' + JSON.stringify(res));
18
25
  })();
19
26
 
@@ -32,10 +32,19 @@ module SvelteOnRails
32
32
  ].join(' ')
33
33
 
34
34
  Dir.chdir(cnf.rails_root) do
35
- stdout, stderr, status = Open3.capture3(cmd, stdin_data: props.to_json, chdir: cnf.rails_root)
35
+ stdout, stderr, status = Open3.capture3(cmd, stdin_data: props.to_json)
36
+
37
+ ary = stdout.split('[svelte-on-rails:successful-json-response]')
38
+
39
+ unless ary.length == 2
40
+ raise "[svelte-on-rails] render ERROR for component: #{@component_files[:svelte_filename]}\n\ncommand:\n+++\n#{cmd}\n+++\n\nstdout:\n+++\n#{stdout}+++\n\n\nstderr:\n+++\n#{stderr}+++"
41
+ end
42
+
36
43
 
37
44
  begin
38
- res = JSON.parse(stdout)
45
+
46
+
47
+ res = JSON.parse(ary[1])
39
48
  css_file = @component_files[:compiled_file] + '.css'
40
49
  if File.exist?(css_file)
41
50
  res['css'] = File.read(css_file)
@@ -50,7 +59,7 @@ module SvelteOnRails
50
59
  return res
51
60
  rescue JSON::ParserError => e
52
61
 
53
- raise "[svelte-on-rails] render ERROR Svelte Server-side: Expected JSON, got: «#{stdout}»\n\ncomponent: #{@component_files[:svelte_filename]}\n\nstdout:\n+++\n#{stderr}+++\n\ncmd: «#{cmd}»"
62
+ raise "[svelte-on-rails] render ERROR Svelte Server-side for component: #{@component_files[:svelte_filename]}\n\nError message:\n+++\n#{e.message}\n+++\n\nstdout:\n+++\n#{stdout}+++\n\n\nstderr:\n+++\n#{stderr}+++"
54
63
  end
55
64
  end
56
65
  end
@@ -0,0 +1,47 @@
1
+ module SvelteOnRails
2
+ class TurboStream
3
+ def dispatch_event(event: 'stream-action', event_detail: nil, selector: nil, component: nil, channel: nil)
4
+
5
+ if event != 'stream-action' && !selector
6
+ raise "Another event name than the default one is only possible together with a selector"
7
+ end
8
+
9
+ args = {
10
+ eventDetail: event_detail,
11
+ component: component,
12
+ event: event,
13
+ selector: selector
14
+ }
15
+
16
+ args_enc = Base64.strict_encode64(args.to_json)
17
+
18
+ Turbo::StreamsChannel.send(
19
+ "broadcast_append_to",
20
+ channel || configs['channel'],
21
+ target: configs['target_html_id'],
22
+ content: "<div style=\"display: none;\" data-controller=\"svelte-on-rails-turbo-stream\" data-args=\"#{args_enc}\"></div>"
23
+ )
24
+
25
+ end
26
+
27
+ private
28
+
29
+ def configs
30
+ @configs ||= begin
31
+
32
+ conf = SvelteOnRails::Configuration.instance
33
+ unless conf.configs[:turbo_stream]
34
+ raise '[svelte-on-rails] missing configuration: :turbo_stream'
35
+ end
36
+ unless conf.configs[:turbo_stream]['target_html_id']
37
+ raise '[svelte-on-rails] missing configuration: turbo_stream/target_html_id'
38
+ end
39
+ unless conf.configs[:turbo_stream]['channel']
40
+ raise '[svelte-on-rails] missing configuration: turbo_stream/channel'
41
+ end
42
+
43
+ conf.configs[:turbo_stream]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -21,6 +21,12 @@ non_ssr_request_header: 'X-Turbo-Request-ID'
21
21
  # expires_in: 90.minutes (=> default / fallback: 1 hour)
22
22
  # namespace: 'my-app-svelte-on-rails'
23
23
 
24
+ turbo_stream:
25
+ # this part you need when you want to be able to trigger javascript actions on svelte-components from the backend
26
+ target_html_id: 'svelte-on-rails-stream-actions-box'
27
+ # html-id of any element that must exist for being able to receive actions (turbo streams can only work this way)
28
+ channel: 'public'
29
+
24
30
  development:
25
31
  watch_changes: true
26
32
  # Check on every request if any file within the svelte components folder have changed, for recompiling
@@ -0,0 +1,9 @@
1
+ class SvelteOnRailsChannel < ApplicationCable::Channel
2
+ def subscribed
3
+ stream_from "svelte_on_rails_channel"
4
+ end
5
+
6
+ def unsubscribed
7
+ # Any cleanup needed when channel is unsubscribed
8
+ end
9
+ end
@@ -3,4 +3,33 @@ class SvelteOnRailsHelloWorldController < ApplicationController
3
3
  def index
4
4
  end
5
5
 
6
+ def turbo_stream_action
7
+
8
+ if params['increase']
9
+ SvelteOnRails::TurboStream.new.dispatch_event(
10
+ event: 'click',
11
+ selector: '.counter-button',
12
+ component: '/javascript/components/ReceiveFromChannel'
13
+ )
14
+ render plain: 'increase-action streamed'
15
+ elsif params['action_cable']
16
+
17
+ ActionCable.server.broadcast("svelte_on_rails_channel",
18
+ {
19
+ eventDetail: { message: "Sent by ActionCable: äöü🤣🌴🌍漢字" },
20
+ component: '/javascript/components/ReceiveFromChannel',
21
+ }
22
+ )
23
+ #SvelteOnRails::ActionCable.new.dispatch_event(channel: "svelte_on_rails_channel", event_detail: { message: "Hello from SvelteChannel!" })
24
+
25
+ render plain: 'ActionCable streaming'
26
+ else
27
+ SvelteOnRails::TurboStream.new.dispatch_event(
28
+ event_detail: { message: "Sent by TurboStream: äöü🤣🌴🌍漢字", user: "Müller" }
29
+ )
30
+ render plain: 'component-action streamed'
31
+ end
32
+
33
+ end
34
+
6
35
  end
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import axios from "axios";
3
+ import {addComponentStreamListener} from '@csedl/svelte-on-rails/src/componentStreamListener'
4
+ import { onMount } from "svelte";
5
+
6
+ let counter = $state(0)
7
+ let results = $state([])
8
+ let incrBtn
9
+
10
+ onMount(() => {
11
+ incrBtn.addEventListener('click', increaseCounter)
12
+ });
13
+
14
+ function callChannelAction(params) {
15
+ results = []
16
+ axios.get(`/svelte_on_rails_hello_world/turbo_stream_action?${params}`)
17
+ .then(function (response) {
18
+ results.push(
19
+ {
20
+ text: `server action called, status: ${response.status}`,
21
+ class: 'called-for-action'
22
+ })
23
+ })
24
+ }
25
+
26
+ function increaseCounter() {
27
+ counter += 1
28
+ }
29
+
30
+ const handleCableEvent = (ev) => {
31
+ results.push(
32
+ {
33
+ text: `Message received from Server: ${JSON.stringify(ev.detail.message)}`,
34
+ class: 'stream-action-received'
35
+ }
36
+ )
37
+ };
38
+
39
+ </script>
40
+
41
+ <h1 use:addComponentStreamListener={handleCableEvent}>Test TurboStreams Channel</h1>
42
+
43
+ <p>Actions that are triggered by SvelteOnRails::TurboStream channel from the server</p>
44
+
45
+ <button onstream-action="{() => callChannelAction('increase=true')}">Call increase action over Channel</button>
46
+
47
+ <button bind:this={incrBtn} class="counter-button" onclick="{increaseCounter}">increase {counter}</button>
48
+
49
+ <button class="call-channel-action" onclick="{callChannelAction}">Action</button>
50
+
51
+ <button onclick="{() => callChannelAction('action_cable=true')}">Call ActionCable Action</button>
52
+
53
+ <ul class="results">
54
+ {#each results as result}
55
+ <li class="{result.class}">{result.text}</li>
56
+ {/each}
57
+ </ul>
@@ -53,7 +53,7 @@
53
53
  -webkit-box-shadow: 2px 3px 16px -1px rgba(0, 0, 0, 0.75);
54
54
  -moz-box-shadow: 2px 3px 16px -1px rgba(0, 0, 0, 0.75);
55
55
  }
56
- img, .wrap-svg {
56
+ img {
57
57
  display: inline-block;
58
58
  width: 30px;
59
59
  height: 30px;
@@ -4,6 +4,8 @@
4
4
  <%= link_to 'Backend/Frontend Test', '/svelte_on_rails_hello_world/backend_frontend_rendered' %>
5
5
  |
6
6
  <%= link_to 'SSR-Auto rendered (default)', '/svelte_on_rails_hello_world/ssr_auto_rendered' %>
7
+ |
8
+ <%= link_to 'Turbo Streams', '/svelte_on_rails_hello_world/turbo_streams_channel' %>
7
9
  </div>
8
10
 
9
11
  <% turbo_id = request.headers['X-Turbo-Request-ID'] %>
@@ -13,4 +13,14 @@
13
13
  width: 32px;
14
14
  height: 32px;
15
15
  }
16
+ .svelte-component {
17
+ border: 1px solid lightgray;
18
+ max-width: 30rem;
19
+ padding: 2rem;
20
+ margin: 2rem;
21
+ }
22
+
23
+ button {
24
+ margin: 4px;
25
+ }
16
26
  </style>
@@ -0,0 +1,12 @@
1
+ <%= turbo_stream_from 'public' %>
2
+
3
+ <%= render 'styles' %>
4
+
5
+
6
+ <%= render 'nav' %>
7
+
8
+
9
+ <%= svelte_component "ReceiveFromChannel" %>
10
+
11
+ <%= content_tag :div, id: 'svelte-on-rails-stream-actions-box' do %>
12
+ <% end %>
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: 4.1.1
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Sedlmair
@@ -31,6 +31,7 @@ files:
31
31
  - README.md
32
32
  - lib/generators/svelte_on_rails/install/install_generator.rb
33
33
  - lib/svelte-on-rails.rb
34
+ - lib/svelte_on_rails/action_cable.rb
34
35
  - lib/svelte_on_rails/configuration.rb
35
36
  - lib/svelte_on_rails/installer/gem_utils.rb
36
37
  - lib/svelte_on_rails/installer/haml.rb
@@ -47,11 +48,13 @@ files:
47
48
  - lib/svelte_on_rails/renderer/render.js
48
49
  - lib/svelte_on_rails/renderer/renderer.rb
49
50
  - lib/svelte_on_rails/renderer/utils.js
51
+ - lib/svelte_on_rails/turbo_stream.rb
50
52
  - lib/svelte_on_rails/view_helpers.rb
51
53
  - lib/tasks/svelte_on_rails_tasks.rake
52
54
  - templates/config_base/app/frontend/ssr/ssr.js
53
55
  - templates/config_base/config/svelte_on_rails.yml
54
56
  - templates/config_base/vite-ssr.config.ts
57
+ - templates/rails_vite_hello_world/app/channels/svelte_on_rails_channel.rb
55
58
  - templates/rails_vite_hello_world/app/controllers/svelte_on_rails_hello_world_controller.rb
56
59
  - templates/rails_vite_hello_world/app/frontend/images/svelte-on-rails-hello-world-england.png
57
60
  - templates/rails_vite_hello_world/app/frontend/images/svelte-on-rails-hello-world-face-smile-wink.svg
@@ -60,6 +63,7 @@ files:
60
63
  - templates/rails_vite_hello_world/app/frontend/javascript/components/JpgImport.svelte
61
64
  - templates/rails_vite_hello_world/app/frontend/javascript/components/ParentWithChild.svelte
62
65
  - templates/rails_vite_hello_world/app/frontend/javascript/components/PngImport.svelte
66
+ - templates/rails_vite_hello_world/app/frontend/javascript/components/ReceiveFromChannel.svelte
63
67
  - templates/rails_vite_hello_world/app/frontend/javascript/components/SvelteOnRailsHelloWorld.svelte
64
68
  - templates/rails_vite_hello_world/app/frontend/javascript/components/SvgRawImport.svelte
65
69
  - templates/rails_vite_hello_world/app/frontend/javascript/components/sub/NestedComponent.svelte
@@ -70,6 +74,7 @@ files:
70
74
  - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/backend_frontend_rendered.html.erb
71
75
  - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/index.html.erb
72
76
  - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/ssr_auto_rendered.html.erb
77
+ - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/turbo_streams_channel.html.erb
73
78
  homepage: https://gitlab.com/sedl/svelte-on-rails
74
79
  licenses:
75
80
  - MIT