svelte-on-rails 5.2.0 → 5.3.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -25
  3. data/lib/generators/svelte_on_rails/install/install_generator.rb +3 -3
  4. data/lib/svelte-on-rails.rb +2 -0
  5. data/lib/svelte_on_rails/action_cable.rb +43 -8
  6. data/lib/svelte_on_rails/active_record_extensions.rb +36 -0
  7. data/lib/svelte_on_rails/installer/hello_world.rb +1 -1
  8. data/lib/svelte_on_rails/lib/utils.rb +152 -0
  9. data/lib/svelte_on_rails/lib/view_helper_support.rb +3 -30
  10. data/lib/svelte_on_rails/railtie.rb +6 -0
  11. data/lib/svelte_on_rails/turbo_stream.rb +42 -2
  12. data/lib/tasks/svelte_on_rails_tasks.rake +2 -2
  13. data/templates/{rails_vite_hello_world → all_features_test}/app/controllers/svelte_on_rails_hello_world_controller.rb +27 -6
  14. data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/ReceiveFromChannel.svelte +25 -8
  15. data/templates/{rails_vite_hello_world → all_features_test}/app/views/svelte_on_rails_hello_world/_nav.html.erb +1 -1
  16. metadata +24 -23
  17. /data/templates/{rails_vite_hello_world → all_features_test}/app/channels/svelte_on_rails_channel.rb +0 -0
  18. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/images/svelte-on-rails-hello-world-england.png +0 -0
  19. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/images/svelte-on-rails-hello-world-face-smile-wink.svg +0 -0
  20. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/images/svelte-on-rails-hello-world-switzerland.jpg +0 -0
  21. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/initializers/actionCable.js +0 -0
  22. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/JavascriptImport.svelte +0 -0
  23. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/JpgImport.svelte +0 -0
  24. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/ParentWithChild.svelte +0 -0
  25. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/PngImport.svelte +0 -0
  26. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/SvelteOnRailsHelloWorld.svelte +0 -0
  27. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/SvgRawImport.svelte +0 -0
  28. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/components/sub/NestedComponent.svelte +0 -0
  29. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/nestedJavascript.js +0 -0
  30. /data/templates/{rails_vite_hello_world → all_features_test}/app/frontend/javascript/nestedJavascriptToggled.js +0 -0
  31. /data/templates/{rails_vite_hello_world → all_features_test}/app/views/svelte_on_rails_hello_world/_styles.html.erb +0 -0
  32. /data/templates/{rails_vite_hello_world → all_features_test}/app/views/svelte_on_rails_hello_world/backend_frontend_rendered.html.erb +0 -0
  33. /data/templates/{rails_vite_hello_world → all_features_test}/app/views/svelte_on_rails_hello_world/index.html.erb +0 -0
  34. /data/templates/{rails_vite_hello_world → all_features_test}/app/views/svelte_on_rails_hello_world/ssr_auto_rendered.html.erb +0 -0
  35. /data/templates/{rails_vite_hello_world/app/views/svelte_on_rails_hello_world/turbo_streams_channel.html.erb → all_features_test/app/views/svelte_on_rails_hello_world/web_socket.html.erb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f7ba72715095d212a289e5fba57a8ddd78b358474e4219a5b3c47f3b7d66c88
4
- data.tar.gz: e690deefa5507a42cf31176efc368c3ff2f304ce1ff057dc79f431e33b5c4232
3
+ metadata.gz: 4b172f3a295136f0317cc6fabb713262b1d1ac813894ef7e07f9b48da61f01a0
4
+ data.tar.gz: 477f5ce8a86e4d0e7bbf508be99486a9763454ee027b43240cb3432cf5250c37
5
5
  SHA512:
6
- metadata.gz: c1b2150fa517ce388feaf13c9816b8d71cb339c9ac9670d7552096430a8cddff8688629ca96c58dbcde715e4f9e9b3953e6760ab6e67c37f3a0ad10797b8fcc9
7
- data.tar.gz: 275691b15976c8dbcd551b3c07ec7495da686c7fa23192b5aca471527b4fe63c20fa8048851a3d9b0085756de52bc7ae1d1e4e5a663114a9fbed9608873ba68e
6
+ metadata.gz: 2f2595ac48833224788450ce49f31d775ffcd552dd628b630caa450045b02a38c725c81fadeaf4833b6eb8134d32b28c7c1b69fd387b639d0c9c655a22549a08
7
+ data.tar.gz: fd6aecc7d1923717b5e0119b337d221d6cf0e5c1e4a6b95f5a2a58911787b0e5a4587022850ea546a2c9e385e7d19a8fd96856ecbb07f010830bbe0d5319641b
data/README.md CHANGED
@@ -5,24 +5,26 @@
5
5
 
6
6
  ---
7
7
 
8
- Combining [DHH's vision](https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision)
9
- with modern front-end requirements:
8
+ **Svelte on Rails: Modern Front-End + Rails Simplicity**
10
9
 
11
- Svelte.
10
+ Guided by our Principles:
12
11
 
13
- Vite.
12
+ - **Minimal JavaScript but Reactive Power**
13
+ - **Beautiful Code, More Joy, Better Results**
14
+ - **Convention over Configuration**
14
15
 
15
- # Why Choose Svelte for Integration?
16
+ `svelte-on-rails` completes [DHH's vision](https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision)
17
+ for modern front-end demands.
16
18
 
17
- Svelte delivers the robust frontend experience missing from DHH’s
18
- vision while aligning seamlessly with Rails’ full-stack philosophy.
19
+ # Why Integrate Svelte?
20
+
21
+ Svelte offers the simplest and most elegant approach to building reactive, high-performance front-end components.
19
22
 
20
23
  - **Seamless Integration**
21
24
  - Works flawlessly with Hotwired/Turbo
22
- - Complements Hotwired perfectly
25
+ - Enhances Hotwire’s capabilities
23
26
  - **Developer-Friendly**
24
- - Easy to learn
25
- - Intuitive and powerful
27
+ - Simple to learn, intuitive, and powerful
26
28
  - Lightning-fast performance
27
29
  - **Compared to Single Page Apps (SPAs)**
28
30
  - Full-stack development delivers maximum value:
@@ -30,17 +32,17 @@ vision while aligning seamlessly with Rails’ full-stack philosophy.
30
32
  - Single-source system delivery
31
33
  - For the most HTML Hotwired is enough
32
34
  - **Compared to Hotwired**
33
- - Stimulus is not a tool for writing frontend-apps
34
- - Svelte eliminates redundant HTML state logic
35
+ - Stimulus is not a tool for frontend-apps
36
+ - Svelte eliminates redundant HTML initial state logic
35
37
  - Consolidates component logic into a single file
36
- - Offloads rendering of certain HTML components to frontend JavaScript, reducing server load
37
- - **Compared to React**
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
38
+ - Offloads rendering to JavaScript by Frontend, Server side Rendering only where necessary, reducing server load
39
+ - **Compared to React/Vue**
40
+ - No virtual DOM, resulting in leaner packages and faster performance
41
+ - See Rich Harris’ [Rethinking Reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity) (3:50–6:40) for a compelling comparison
42
+ - Easier to learn
43
+ - While React and Vue have larger communities, Svelte’s ecosystem is robust and growing, ideal for Rails integration
42
44
 
43
- Svelte empowers Rails’ full-stack vision with modern, streamlined frontend integration.
45
+ Svelte empowers Rails’ full-stack vision with modern, efficient front-end integration.
44
46
 
45
47
  # Features
46
48
 
@@ -307,6 +309,46 @@ Consider setting `ssr: false` for testing only for performance reasons.
307
309
  Yo can override this on a attribute on the view-helper for specific components.
308
310
  But, in normal cases it should not be neccessary testing ssr explicitly.
309
311
 
312
+ ## ActiveRecord helpers
313
+
314
+ Adds the `#svelte_attributes` helper to your models, example:
315
+
316
+ ```ruby
317
+ @book.svelte_attributes(
318
+ :name,
319
+ :calculation_method,
320
+ author: [:name]
321
+ )
322
+ ```
323
+
324
+ would result in something like this:
325
+
326
+ ```ruby
327
+ {
328
+ "values" => {
329
+ "name" => "Learning Ruby",
330
+ "calculation_method": "any-result",
331
+ "author" => {
332
+ "name" => "Michael Hartl"
333
+ },
334
+ },
335
+ "book_labels" => {
336
+ "name" => "Name", # translated by human_attribute_name..
337
+ "calculation_method" => "Calculation method",
338
+ "author" => "Author"
339
+ },
340
+ "author_labels": {
341
+ "name" => "Name"
342
+ }
343
+ }
344
+ ```
345
+
346
+ This should ease transferring data you need within the component mostly.
347
+
348
+ If an optional `belongs_to` association is empty, the labels are still calculated.
349
+
350
+ **Caching:** The component's attributes are used to generate a checksum, which serves as the cache key for efficient storage and retrieval.
351
+
310
352
  ## Caching
311
353
 
312
354
  Caching only is relevant for `ssr`
@@ -375,7 +417,7 @@ turbo_stream:
375
417
  over streams or have different channels for each user privileges
376
418
  - Has [Compatibility issue with UJS](https://github.com/hotwired/turbo-rails?tab=readme-ov-file#compatibility-with-rails-ujs)
377
419
 
378
- ## SvelteOnRails::ActionCable.send
420
+ ## SvelteOnRails::ActionCable
379
421
 
380
422
  ActionCable is the more basic library behind `TurboStream` and it is a second Option to call Javascript Actions from the server.
381
423
 
@@ -453,7 +495,7 @@ The «wrapping Element» is the Element from the view helper `svelte_component`
453
495
  Now you can dispatch events on the component by:
454
496
 
455
497
  ```ruby
456
- SvelteOnRails::ActionCable.send(
498
+ SvelteOnRails::ActionCable.dispatch(
457
499
  'folder/MyComponent',
458
500
  { message: "greetings from Server: äöü🤣🌴🌍漢字" }
459
501
  )
@@ -461,9 +503,12 @@ SvelteOnRails::ActionCable.send(
461
503
 
462
504
  And you will find the object, with the message key on the browser logs.
463
505
 
464
- Without any arguments, just by `SvelteOnRails::ActionCable.send` it would fire the `stream-action` event on all components.
506
+ Without any arguments, just by `SvelteOnRails::ActionCable.dispatch` it would fire the `stream-action` event on all components.
465
507
 
466
- # SvelteOnRails::TurboStream.send
508
+ The **#dispatch_by_selector** does not go over the component, it searches for any matching selector just
509
+ on the whole `document` and fires the given event there.
510
+
511
+ # SvelteOnRails::TurboStream
467
512
 
468
513
  Turbo Stream makes more sense when you think of sending confidential data to the components
469
514
  or you want to separate to channels based on user groups, for example.
@@ -501,7 +546,7 @@ When this works you are good to go.
501
546
  And call this by:
502
547
 
503
548
  ```ruby
504
- SvelteOnRails::TurboStream.send
549
+ SvelteOnRails::TurboStream.dispatch
505
550
  ```
506
551
  **What it does**
507
552
 
@@ -527,7 +572,7 @@ within the svelte component.
527
572
  Then, call it by:
528
573
 
529
574
  ```ruby
530
- SvelteOnRails::TurboStream.new.dispatch_event(
575
+ SvelteOnRails::TurboStream.dispatch(
531
576
  channel: 'my-custom-stream',
532
577
  component: '/javascript/components/TurboStreamsChannel',
533
578
  selector: '.counter-button',
@@ -536,6 +581,9 @@ Then, call it by:
536
581
  )
537
582
  ```
538
583
 
584
+ The **#dispatch_by_selector** does not go over the component, it searches for any matching selector just
585
+ on the whole `document` and fires the given event there.
586
+
539
587
  ## More rake tasks
540
588
 
541
589
  This tasks are more for testing/playground purposes
@@ -129,15 +129,15 @@ module SvelteOnRails
129
129
  hw_i = SvelteOnRails::Installer::HelloWorld
130
130
 
131
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)
132
+ utils_i.add_route(" get \"svelte_on_rails_hello_world/web_socket\"", app_root: Rails.root)
133
+ utils_i.add_route(" get \"svelte_on_rails_hello_world/web_socket_action\"", app_root: Rails.root)
134
134
  npm_i = SvelteOnRails::Installer::Npm
135
135
  npm_i.install_or_update_package('axios')
136
136
  npm_i.install_or_update_package('@rails/actioncable')
137
137
  js_i = SvelteOnRails::Installer::Javascript
138
138
  init_stat = '../initializers/actionCable.js'
139
139
  js_i.append_import_statement(application_js_path, init_stat, "import '#{init_stat}';")
140
- @hello_world_path = hw_i.install_hello_world(['rails_vite_hello_world'], app_root: nil, force: true, silent: true)
140
+ @hello_world_path = hw_i.install_hello_world(['all_features_test'], app_root: nil, force: true, silent: true)
141
141
  end
142
142
 
143
143
  def haml_and_convert
@@ -2,6 +2,8 @@ require "svelte_on_rails/configuration"
2
2
  require "svelte_on_rails/view_helpers"
3
3
  require "svelte_on_rails/turbo_stream"
4
4
  require "svelte_on_rails/action_cable"
5
+ require "svelte_on_rails/active_record_extensions"
6
+
5
7
  require "svelte_on_rails/railtie" if defined?(Rails)
6
8
 
7
9
  require "svelte_on_rails/renderer/renderer"
@@ -1,11 +1,16 @@
1
1
  module SvelteOnRails
2
2
  class ActionCable
3
- def self.send(component = nil, event_detail = nil, event: 'stream-action', selector: nil, channel: nil)
3
+ def self.dispatch(component = nil, event_detail = nil, event: 'stream-action', selector: nil, channel: nil)
4
4
 
5
- _channel = configs['channel'] || channel
6
- unless _channel.present?
7
- raise 'Missing attribute or configuration: action_cable/channel'
8
- end
5
+ utils = SvelteOnRails::Lib::Utils
6
+ conf = SvelteOnRails::Configuration.instance
7
+
8
+ _comp = if component
9
+ utils.validate_filename(component)
10
+ "/#{conf.components_folder + component}"
11
+ end
12
+
13
+ _channel = require_channel(svelte_on_rails_configs['channel'] || channel)
9
14
 
10
15
  if event != 'stream-action' && !selector
11
16
  raise "Another event name than the default one is only possible together with a selector"
@@ -13,7 +18,7 @@ module SvelteOnRails
13
18
 
14
19
  args = {
15
20
  eventDetail: event_detail,
16
- component: component,
21
+ component: _comp,
17
22
  event: event,
18
23
  selector: selector
19
24
  }
@@ -23,13 +28,43 @@ module SvelteOnRails
23
28
  args
24
29
  )
25
30
 
31
+ end
32
+
33
+ def self.dispatch_by_selector(selector, event_detail = nil, event: 'stream-action', channel: nil)
34
+
35
+ _channel = require_channel(svelte_on_rails_configs['channel'] || channel)
36
+
37
+ if event != 'stream-action' && !selector
38
+ raise "Another event name than the default one is only possible together with a selector"
39
+ end
40
+
41
+ args = {
42
+ eventDetail: event_detail,
43
+ component: '/false/',
44
+ event: event,
45
+ selector: selector
46
+ }
47
+
48
+ ::ActionCable.server.broadcast(
49
+ _channel,
50
+ args
51
+ )
26
52
 
27
53
  end
28
54
 
29
55
  private
30
56
 
31
- def self.configs
32
- SvelteOnRails::Configuration.instance.configs[:action_cable]
57
+ def self.svelte_on_rails_configs
58
+ cnf = SvelteOnRails::Configuration.instance.configs[:action_cable]
59
+ raise 'ActionCable not configured for SvelteOnRails' if cnf.nil?
60
+ cnf
61
+ end
62
+
63
+ def self.require_channel(channel)
64
+ unless channel.present?
65
+ raise 'Missing attribute or configuration: action_cable/channel'
66
+ end
67
+ channel
33
68
  end
34
69
 
35
70
  end
@@ -0,0 +1,36 @@
1
+ # lib/svelte_on_rails/active_record_extensions.rb
2
+ module SvelteOnRails
3
+ module ActiveRecordExtensions
4
+ def self.included(base)
5
+ unless defined?(ActiveRecord::Base) && base.ancestors.include?(ActiveRecord::Base)
6
+ raise 'SvelteOnRails::ActiveRecordExtensions can only be included in ActiveRecord models'
7
+ end
8
+ end
9
+
10
+ # Returns a hash of attributes, methods, and associations formatted for Svelte components
11
+ def svelte_attributes(*attributes)
12
+ @svelte_attributes ||= begin
13
+
14
+ # separate offset and limit
15
+
16
+ opts = attributes.grep(Hash).first.symbolize_keys
17
+ offset = opts[:offset]
18
+ limit = opts[:limit]
19
+ attr = if offset || limit
20
+ unless self.respond_to?(:each)
21
+ raise 'offset and limit are only supported for record sets that respond to :each'
22
+ end
23
+ _opts = opts.reject { |key, _| [:offset, :limit].include?(key) }
24
+ attributes.select { |item| !item.is_a?(Hash) }.push(_opts)
25
+ else
26
+ attributes
27
+ end
28
+
29
+ utils = SvelteOnRails::Lib::Utils
30
+ utils.svelte_attributes(self, attr, offset: offset, limit: limit)
31
+
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -38,7 +38,7 @@ module SvelteOnRails
38
38
  end
39
39
 
40
40
 
41
- def self.remove_hello_world(templates = ['rails_vite_hello_world'], app_root: nil, ask: true)
41
+ def self.remove_hello_world(templates = ['all_features_test'], app_root: nil, ask: true)
42
42
 
43
43
  utils = SvelteOnRails::Installer::Utils
44
44
 
@@ -137,6 +137,158 @@ module SvelteOnRails
137
137
  Rails.logger.warn(" => #{line}")
138
138
  end
139
139
  end
140
+
141
+ def self.validate_filename(filename)
142
+
143
+ conf = SvelteOnRails::Configuration.instance
144
+
145
+ file_path = conf.components_folder_full + "#{filename}.svelte"
146
+
147
+ unless File.exist?(file_path)
148
+
149
+ raise "svelte-on-rails gem, view helper #svelte_component\n\nFile not found:\n" +
150
+ "#{conf.components_folder_full + "#{filename}.svelte"}\n\n" +
151
+ "Your configurations are:\n\nfrontend_folder: «#{conf.frontend_folder}»\ncomponents_folder: «#{conf.components_folder}»\n.. and the filename attribute for the view helper: «#{filename}»\n"
152
+
153
+ end
154
+
155
+ if conf.watch_changes? && !file_exist_case_sensitive?(conf.components_folder_full, "#{filename}.svelte")
156
+
157
+ raise "svelte-on-rails gem, view helper #svelte_component\n\n" +
158
+ "File found but Upper and lower case letters are incorrect:\n" +
159
+ "(This check is only on development environments when watch_changes is true)\n\n" +
160
+ "#{conf.components_folder_full + "#{filename}.svelte"}\n\n" +
161
+ "Your configurations are:\nfrontend_folder: «#{conf.frontend_folder}»\ncomponents_folder: «#{conf.components_folder}»\n.. and the filename attribute for the view helper: «#{filename}»\n"
162
+
163
+ end
164
+ end
165
+
166
+ def self.svelte_attributes(record, attributes, labels: {}, call_stack: 0, offset: nil, limit: nil)
167
+
168
+ if record.is_a?(Class)
169
+
170
+ # this is the case if a belongs_to association is empty; In that case we pass the class itself and extract only the labels
171
+ raise 'limit and offset are supported only for iterable objects' if limit || offset
172
+ attributes.each do |attr|
173
+ unless attr.respond_to?(:each)
174
+ labels["#{record.to_s.underscore}_labels"] ||= {}
175
+ labels["#{record.to_s.underscore}_labels"][attr.to_s] ||= record.human_attribute_name(attr)
176
+ end
177
+ end
178
+ values = {}
179
+
180
+ elsif record.respond_to?(:each)
181
+
182
+ unless record.is_a?(ActiveRecord::Relation) || record.is_a?(ActiveRecord::Associations::CollectionProxy)
183
+ raise 'record set must be a Article::ActiveRecord_Associations_CollectionProxy'
184
+ end
185
+
186
+ recs = (offset ? record.offset(offset) : record)
187
+ recs2 = (limit ? recs.limit(limit) : recs)
188
+
189
+ values = recs2.map do |rec|
190
+ svelte_attributes(rec, attributes, labels: labels, call_stack: call_stack + 1)
191
+ end
192
+
193
+ else
194
+
195
+ # we have a single record
196
+
197
+ raise 'limit and offset are supported only for iterable objects' if limit || offset
198
+ values = {}
199
+
200
+ table_name = record.class.to_s.underscore
201
+
202
+ attributes.each do |attr|
203
+
204
+ if attr.is_a? Hash
205
+
206
+ # we have associations
207
+ attr.each do |key, value|
208
+ _key = key.to_s
209
+
210
+ # skip and fetch offset and limit
211
+ if key.to_s.match(/_limit$/) && !record.respond_to?(key)
212
+ raise 'Invalid attribute' unless record.respond_to?(key.to_s[0..-7])
213
+ next
214
+ end
215
+ if key.to_s.match(/_offset$/) && !record.respond_to?(key)
216
+ raise 'Invalid attribute' unless record.respond_to?(key.to_s[0..-8])
217
+ next
218
+ end
219
+ offs, lim = svelte_attribute_extract_limit(record, attributes, _key)
220
+
221
+ labels["#{table_name}_labels"] ||= {}
222
+ labels["#{table_name}_labels"][_key] ||= record.class.human_attribute_name(_key)
223
+
224
+ # values
225
+
226
+ reflect = record.class.reflect_on_association(_key)
227
+ if reflect
228
+ recs = record.send(_key)
229
+ content = (recs.present? ? recs : reflect.active_record) # if no record, we extract only the labels
230
+ if content.respond_to?(:each)
231
+ values[_key] = svelte_attributes(
232
+ content,
233
+ value,
234
+ labels: labels,
235
+ call_stack: call_stack + 1,
236
+ offset: offs,
237
+ limit: lim
238
+ )
239
+ else
240
+ values[_key] = svelte_attributes(
241
+ content,
242
+ value,
243
+ labels: labels,
244
+ call_stack: call_stack + 1,
245
+ offset: offs,
246
+ limit: lim
247
+ )
248
+ end
249
+ else
250
+ raise "invalid association: #{_key}"
251
+ end
252
+ end
253
+
254
+ else
255
+ # we have attributes
256
+ raise 'Invalid attribute' unless [Symbol, String].include?(attr.class)
257
+ _key = attr.to_s
258
+
259
+ labels["#{table_name}_labels"] ||= {}
260
+ labels["#{table_name}_labels"][_key] ||= record.class.human_attribute_name(_key)
261
+
262
+ values[_key] = record.send(_key)
263
+ end
264
+
265
+ end
266
+ end
267
+
268
+ if call_stack >= 1
269
+ values
270
+ else
271
+ { 'values' => values }.merge(labels)
272
+ end
273
+
274
+ end
275
+
276
+ private
277
+
278
+ def self.svelte_attribute_extract_limit(record, attributes, key)
279
+
280
+ hash_args = attributes.grep(Hash).first.with_indifferent_access # multiple arrays is not possible
281
+
282
+ offset = if hash_args["#{key}_offset"] && !record.respond_to?("#{key}_offset")
283
+ hash_args["#{key}_offset"]
284
+ end
285
+
286
+ limit = if hash_args["#{key}_limit"] && !record.respond_to?("#{key}_limit")
287
+ hash_args["#{key}_limit"]
288
+ end
289
+
290
+ [offset, limit]
291
+ end
140
292
  end
141
293
  end
142
294
  end
@@ -8,9 +8,11 @@ module SvelteOnRails
8
8
  def initialize(file, args, request, caching = false)
9
9
 
10
10
  @start_time = Time.now
11
+ @conf = SvelteOnRails::Configuration.instance
12
+ utils = SvelteOnRails::Lib::Utils
13
+ utils.validate_filename(file) if @conf.watch_changes?
11
14
  @filename = (file.match(/\.svelte$/) ? file[0..-8] : file)
12
15
  @args_checksum = Zlib.crc32(args.to_json).to_s(36)
13
- @conf = SvelteOnRails::Configuration.instance
14
16
  @helper_options, @html_options, @svelte_props = split_props(
15
17
  args,
16
18
  %i[class id style],
@@ -18,7 +20,6 @@ module SvelteOnRails
18
20
  )
19
21
  @request = request
20
22
  @ssr = determine_ssr
21
- validate_file if @conf.watch_changes?
22
23
 
23
24
  # precompile
24
25
 
@@ -64,24 +65,6 @@ module SvelteOnRails
64
65
  @ssr
65
66
  end
66
67
 
67
- def file_not_found_message
68
- ff = conf.frontend_folder
69
- cf = conf.components_folder
70
- "svelte-on-rails gem, view helper #svelte_component\n\nFile not found:\n" +
71
- "#{conf.components_folder_full + "#{filename}.svelte"}\n\n" +
72
- "Your configurations are:\n\nfrontend_folder: «#{ff}»\ncomponents_folder: «#{cf}»\n.. and the filename attribute for the view helper: «#{filename}»\n"
73
- end
74
-
75
- def file_case_sensitive_message
76
- ff = conf.frontend_folder
77
- cf = conf.components_folder
78
- "svelte-on-rails gem, view helper #svelte_component\n\n" +
79
- "File found but Upper and lower case letters are incorrect:\n" +
80
- "(This check is only on development environments when watch_changes is true)\n\n" +
81
- "#{conf.components_folder_full + "#{filename}.svelte"}\n\n" +
82
- "Your configurations are:\nfrontend_folder: «#{ff}»\ncomponents_folder: «#{cf}»\n.. and the filename attribute for the view helper: «#{filename}»\n"
83
- end
84
-
85
68
  def log_rendering(message)
86
69
 
87
70
  Rails.logger.info " #{message} (#{elapsed_milliseconds}ms)"
@@ -119,16 +102,6 @@ module SvelteOnRails
119
102
 
120
103
  private
121
104
 
122
- def validate_file
123
- file_path = conf.components_folder_full + "#{filename}.svelte"
124
- unless File.exist?(file_path)
125
- raise file_not_found_message
126
- end
127
- if conf.watch_changes? && !SvelteOnRails::Lib::Utils.file_exist_case_sensitive?(conf.components_folder_full, "#{filename}.svelte")
128
- raise file_case_sensitive_message
129
- end
130
- end
131
-
132
105
  def determine_ssr
133
106
  _ssr = if @helper_options.key?(:ssr)
134
107
  if @conf.watch_changes?
@@ -13,6 +13,12 @@ module SvelteOnRails
13
13
  SvelteOnRails::Configuration.instance
14
14
  end
15
15
 
16
+ initializer 'svelte_on_rails.active_record_extensions' do
17
+ ActiveSupport.on_load(:active_record) do
18
+ include SvelteOnRails::ActiveRecordExtensions
19
+ end
20
+ end
21
+
16
22
  rake_tasks do
17
23
  load File.expand_path("../../tasks/svelte_on_rails_tasks.rake", __FILE__)
18
24
  end
@@ -1,6 +1,14 @@
1
1
  module SvelteOnRails
2
2
  class TurboStream
3
- def self.send(component = nil, event_detail = nil, event: 'stream-action', selector: nil, channel: nil)
3
+ def self.dispatch(component = nil, event_detail = nil, event: 'stream-action', selector: nil, channel: nil)
4
+
5
+ utils = SvelteOnRails::Lib::Utils
6
+ conf = SvelteOnRails::Configuration.instance
7
+
8
+ _comp = if component
9
+ utils.validate_filename(component)
10
+ "/#{conf.components_folder + component}"
11
+ end
4
12
 
5
13
  if event != 'stream-action' && !selector
6
14
  raise "Another event name than the default one is only possible together with a selector"
@@ -8,7 +16,7 @@ module SvelteOnRails
8
16
 
9
17
  args = {
10
18
  eventDetail: event_detail,
11
- component: component,
19
+ component: _comp,
12
20
  event: event,
13
21
  selector: selector
14
22
  }
@@ -24,6 +32,30 @@ module SvelteOnRails
24
32
 
25
33
  end
26
34
 
35
+ def self.dispatch_by_selector(selector, event_detail = nil, event: 'stream-action', channel: nil)
36
+
37
+ if event != 'stream-action' && !selector
38
+ raise "Another event name than the default one is only possible together with a selector"
39
+ end
40
+
41
+ args = {
42
+ eventDetail: event_detail,
43
+ component: '/false/',
44
+ event: event,
45
+ selector: selector
46
+ }
47
+
48
+ args_enc = Base64.strict_encode64(args.to_json)
49
+
50
+ Turbo::StreamsChannel.send(
51
+ "broadcast_append_to",
52
+ require_channel(channel || configs['channel']),
53
+ target: configs['target_html_id'],
54
+ content: "<div style=\"display: none;\" data-controller=\"svelte-on-rails-turbo-stream\" data-args=\"#{args_enc}\"></div>"
55
+ )
56
+
57
+ end
58
+
27
59
  private
28
60
 
29
61
  def self.configs
@@ -43,5 +75,13 @@ module SvelteOnRails
43
75
  conf.configs[:turbo_stream]
44
76
  end
45
77
  end
78
+
79
+ def self.require_channel(channel)
80
+ unless channel.present?
81
+ raise 'Missing attribute or configuration: turbo_stream/channel'
82
+ end
83
+ channel
84
+ end
85
+
46
86
  end
47
87
  end
@@ -18,7 +18,7 @@ namespace :svelte_on_rails do
18
18
  task :remove_hello_world do
19
19
 
20
20
  hw_i = SvelteOnRails::Installer::HelloWorld
21
- hw_i.remove_hello_world(['rails_vite_hello_world'])
21
+ hw_i.remove_hello_world(['all_features_test'])
22
22
 
23
23
  end
24
24
 
@@ -27,7 +27,7 @@ namespace :svelte_on_rails do
27
27
 
28
28
  puts '-' * 80
29
29
  hw_i = SvelteOnRails::Installer::HelloWorld
30
- hello_world_path = hw_i.install_hello_world(['rails_vite_hello_world'])
30
+ hello_world_path = hw_i.install_hello_world(['all_features_test'])
31
31
  puts "You can now see the Hello World component on: #{hello_world_path}."
32
32
 
33
33
  end
@@ -3,43 +3,64 @@ class SvelteOnRailsHelloWorldController < ApplicationController
3
3
  def index
4
4
  end
5
5
 
6
- def turbo_stream_action
6
+ def web_socket_action
7
7
 
8
- comp = '/javascript/components/ReceiveFromChannel'
8
+ # render plain: Article.first.svelte_attributes(:name) #(:name, :calc_method, children: [:name], children_limit: 2, parent: [:name]).to_json
9
+ #
10
+ # return
11
+
12
+ comp = 'ReceiveFromChannel'
9
13
 
10
14
  case params['stream']
11
15
 
12
16
  when 'action-cable-to-component'
13
- SvelteOnRails::ActionCable.send(
17
+ SvelteOnRails::ActionCable.dispatch(
14
18
  comp,
15
19
  { message: "#{SecureRandom.hex(2)} Sent by <span class='transfer'>ActionCable</span>: äöü🤣🌴🌍漢字", class: 'action-cable-to-component' },
16
20
  event: 'stream-action'
17
21
  )
18
22
 
19
23
  when 'action-cable-to-element'
20
- SvelteOnRails::ActionCable.send(
24
+ SvelteOnRails::ActionCable.dispatch(
21
25
  comp,
22
26
  { message: "#{SecureRandom.hex(2)} <span class='transfer'>ActionCable to .my-custom-class / my-custom-event</span>", class: 'action-cable-to-element' },
23
27
  selector: '.my-custom-class',
24
28
  event: 'my-custom-event'
25
29
  )
26
30
 
31
+ when 'action-cable-to-selector'
32
+ SvelteOnRails::ActionCable.dispatch_by_selector(
33
+ '.receive-by-selector',
34
+ { message: "Sent by ActionCable/Selector: äöü🤣🌴🌍漢字", class: 'action-cable-to-selector' }
35
+ )
36
+
27
37
  when 'turbo-stream-to-all-components'
28
- SvelteOnRails::TurboStream.send(
38
+ SvelteOnRails::TurboStream.dispatch(
29
39
  nil,
30
40
  { message: "Sent by TurboStream: äöü🤣🌴🌍漢字", class: 'turbo-stream-to-all-components' },
31
41
  )
32
42
 
33
43
  when 'turbo-stream-to-element'
34
- SvelteOnRails::TurboStream.send(
44
+ SvelteOnRails::TurboStream.dispatch(
35
45
  nil,
36
46
  { message: "Sent by TurboStream: äöü🤣🌴🌍漢字", class: 'turbo-stream-to-element' },
37
47
  selector: '.my-custom-class',
38
48
  event: 'my-custom-event'
39
49
  )
40
50
 
51
+ when 'turbo-stream-to-selector'
52
+ SvelteOnRails::TurboStream.dispatch_by_selector(
53
+ '.receive-by-selector',
54
+ { message: "Sent by TurboStream/Selector: äöü🤣🌴🌍漢字", class: 'turbo-stream-to-selector' }
55
+ )
56
+
57
+ else
58
+ raise 'Unknown stream'
59
+
41
60
  end
42
61
 
62
+ render plain: "dispatched: #{params['stream']}"
63
+
43
64
  end
44
65
 
45
66
  end
@@ -6,7 +6,7 @@
6
6
 
7
7
  function callChannelAction(action) {
8
8
  results = []
9
- axios.get(`/svelte_on_rails_hello_world/turbo_stream_action?stream=${action}`)
9
+ axios.get(`/svelte_on_rails_hello_world/web_socket_action?stream=${action}`)
10
10
  .then(function (response) {
11
11
  console.log(`server action called, status: ${response.status}`)
12
12
  })
@@ -30,21 +30,38 @@
30
30
  )
31
31
  };
32
32
 
33
+ function handleStreamSelectorEvent(ev) {
34
+ results.push(
35
+ {
36
+ text: `Element triggered from Server: ${JSON.stringify(ev.detail.message)}`,
37
+ class: ev.detail.class
38
+ }
39
+ )
40
+ }
41
+
33
42
  </script>
34
43
 
35
- <h1 use:addComponentStreamListener={handleCableEvent}>Test TurboStreams Channel</h1>
44
+ <h1 use:addComponentStreamListener={handleCableEvent}>Dispatch Actions from Server</h1>
45
+
46
+ <p>Actions that are triggered from the server, by TurboStream or ActionCable</p>
36
47
 
37
- <p>Actions that are triggered by SvelteOnRails::TurboStream channel from the server</p>
48
+ <h3>ActionCable</h3>
49
+ <p>Easier Technic, more basic</p>
50
+ <button class="action-cable-to-component" onclick="{() => callChannelAction('action-cable-to-component')}">to component</button>
51
+ <button class="action-cable-to-element" onclick="{() => callChannelAction('action-cable-to-element')}">to element</button>
52
+ <button class="action-cable-to-element" onclick="{() => callChannelAction('action-cable-to-selector')}">to selector</button>
38
53
 
39
- <button class="action-cable-to-component" onclick="{() => callChannelAction('action-cable-to-component')}">ActionCable to component</button>
40
- <button class="action-cable-to-element" onclick="{() => callChannelAction('action-cable-to-element')}">ActionCable to element</button>
41
- <button class="turbo-stream-to-all-components" onclick="{() => callChannelAction('turbo-stream-to-all-components')}">TurboStream to all components</button>
42
- <button class="turbo-stream-to-element" onclick="{() => callChannelAction('turbo-stream-to-element')}">TurboStream to element</button>
54
+ <h3>TurboStream</h3>
55
+ <p>Securer Channels, for example if sending confidential data from the server to privileged user groups</p>
56
+ <button class="turbo-stream-to-all-components" onclick="{() => callChannelAction('turbo-stream-to-all-components')}">to all components</button>
57
+ <button class="turbo-stream-to-element" onclick="{() => callChannelAction('turbo-stream-to-element')}">to element</button>
58
+ <button class="turbo-stream-to-element" onclick="{() => callChannelAction('turbo-stream-to-selector')}">to selector</button>
43
59
 
44
60
  <span class="my-custom-class" onmy-custom-event="{handleElementEvent}"></span>
61
+ <span class="receive-by-selector" onstream-action="{handleStreamSelectorEvent}"></span>
45
62
 
63
+ <h3>Results</h3>
46
64
  <div class="results-box">
47
- <p>Result:</p>
48
65
  <ul class="results">
49
66
  {#each results as result}
50
67
  <li class="{result.class}">{@html result.text}</li>
@@ -5,7 +5,7 @@
5
5
  |
6
6
  <%= link_to 'SSR-Auto rendered (default)', '/svelte_on_rails_hello_world/ssr_auto_rendered' %>
7
7
  |
8
- <%= link_to 'Turbo Streams', '/svelte_on_rails_hello_world/turbo_streams_channel' %>
8
+ <%= link_to 'Turbo Streams', '/svelte_on_rails_hello_world/web_socket' %>
9
9
  </div>
10
10
 
11
11
  <% turbo_id = request.headers['X-Turbo-Request-ID'] %>
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: 5.2.0
4
+ version: 5.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Sedlmair
@@ -32,6 +32,7 @@ files:
32
32
  - lib/generators/svelte_on_rails/install/install_generator.rb
33
33
  - lib/svelte-on-rails.rb
34
34
  - lib/svelte_on_rails/action_cable.rb
35
+ - lib/svelte_on_rails/active_record_extensions.rb
35
36
  - lib/svelte_on_rails/configuration.rb
36
37
  - lib/svelte_on_rails/installer/gem_utils.rb
37
38
  - lib/svelte_on_rails/installer/haml.rb
@@ -51,31 +52,31 @@ files:
51
52
  - lib/svelte_on_rails/turbo_stream.rb
52
53
  - lib/svelte_on_rails/view_helpers.rb
53
54
  - lib/tasks/svelte_on_rails_tasks.rake
55
+ - templates/all_features_test/app/channels/svelte_on_rails_channel.rb
56
+ - templates/all_features_test/app/controllers/svelte_on_rails_hello_world_controller.rb
57
+ - templates/all_features_test/app/frontend/images/svelte-on-rails-hello-world-england.png
58
+ - templates/all_features_test/app/frontend/images/svelte-on-rails-hello-world-face-smile-wink.svg
59
+ - templates/all_features_test/app/frontend/images/svelte-on-rails-hello-world-switzerland.jpg
60
+ - templates/all_features_test/app/frontend/initializers/actionCable.js
61
+ - templates/all_features_test/app/frontend/javascript/components/JavascriptImport.svelte
62
+ - templates/all_features_test/app/frontend/javascript/components/JpgImport.svelte
63
+ - templates/all_features_test/app/frontend/javascript/components/ParentWithChild.svelte
64
+ - templates/all_features_test/app/frontend/javascript/components/PngImport.svelte
65
+ - templates/all_features_test/app/frontend/javascript/components/ReceiveFromChannel.svelte
66
+ - templates/all_features_test/app/frontend/javascript/components/SvelteOnRailsHelloWorld.svelte
67
+ - templates/all_features_test/app/frontend/javascript/components/SvgRawImport.svelte
68
+ - templates/all_features_test/app/frontend/javascript/components/sub/NestedComponent.svelte
69
+ - templates/all_features_test/app/frontend/javascript/nestedJavascript.js
70
+ - templates/all_features_test/app/frontend/javascript/nestedJavascriptToggled.js
71
+ - templates/all_features_test/app/views/svelte_on_rails_hello_world/_nav.html.erb
72
+ - templates/all_features_test/app/views/svelte_on_rails_hello_world/_styles.html.erb
73
+ - templates/all_features_test/app/views/svelte_on_rails_hello_world/backend_frontend_rendered.html.erb
74
+ - templates/all_features_test/app/views/svelte_on_rails_hello_world/index.html.erb
75
+ - templates/all_features_test/app/views/svelte_on_rails_hello_world/ssr_auto_rendered.html.erb
76
+ - templates/all_features_test/app/views/svelte_on_rails_hello_world/web_socket.html.erb
54
77
  - templates/config_base/app/frontend/ssr/ssr.js
55
78
  - templates/config_base/config/svelte_on_rails.yml
56
79
  - templates/config_base/vite-ssr.config.ts
57
- - templates/rails_vite_hello_world/app/channels/svelte_on_rails_channel.rb
58
- - templates/rails_vite_hello_world/app/controllers/svelte_on_rails_hello_world_controller.rb
59
- - templates/rails_vite_hello_world/app/frontend/images/svelte-on-rails-hello-world-england.png
60
- - templates/rails_vite_hello_world/app/frontend/images/svelte-on-rails-hello-world-face-smile-wink.svg
61
- - templates/rails_vite_hello_world/app/frontend/images/svelte-on-rails-hello-world-switzerland.jpg
62
- - templates/rails_vite_hello_world/app/frontend/initializers/actionCable.js
63
- - templates/rails_vite_hello_world/app/frontend/javascript/components/JavascriptImport.svelte
64
- - templates/rails_vite_hello_world/app/frontend/javascript/components/JpgImport.svelte
65
- - templates/rails_vite_hello_world/app/frontend/javascript/components/ParentWithChild.svelte
66
- - templates/rails_vite_hello_world/app/frontend/javascript/components/PngImport.svelte
67
- - templates/rails_vite_hello_world/app/frontend/javascript/components/ReceiveFromChannel.svelte
68
- - templates/rails_vite_hello_world/app/frontend/javascript/components/SvelteOnRailsHelloWorld.svelte
69
- - templates/rails_vite_hello_world/app/frontend/javascript/components/SvgRawImport.svelte
70
- - templates/rails_vite_hello_world/app/frontend/javascript/components/sub/NestedComponent.svelte
71
- - templates/rails_vite_hello_world/app/frontend/javascript/nestedJavascript.js
72
- - templates/rails_vite_hello_world/app/frontend/javascript/nestedJavascriptToggled.js
73
- - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/_nav.html.erb
74
- - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/_styles.html.erb
75
- - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/backend_frontend_rendered.html.erb
76
- - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/index.html.erb
77
- - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/ssr_auto_rendered.html.erb
78
- - templates/rails_vite_hello_world/app/views/svelte_on_rails_hello_world/turbo_streams_channel.html.erb
79
80
  homepage: https://gitlab.com/sedl/svelte-on-rails
80
81
  licenses:
81
82
  - MIT