render_turbo_stream 2.1.2 → 3.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: 92144d161ab6a0eee72442b7b848a140b9f439cbda17a3e41ad4a28aa5f0a8cb
4
- data.tar.gz: 3b290864b96789968ab8ad704b1127cb37ab6c00985c11c1cd87c28a2cb87695
3
+ metadata.gz: ef24460b794e7db6757f2ad199e94f9ce7df38615b9ed64bcdcfc0af978cf573
4
+ data.tar.gz: 1c14768ff0ee5277befcb9d039c37b7df5e549cf3f23910672583099ab387590
5
5
  SHA512:
6
- metadata.gz: ea284231a3dd9343643a868bde136c954e32d0347354c5127f83b64e297e81309372214490df90580f32143c9a0f1422fe8f6abb2e2e1b9f7096726fb1454896
7
- data.tar.gz: c8719357d38e4ced162e9bc2b94401ee3e7096f6097e1402b9ac9670c3b124326da0a9ac858b9ff5a434458efd010fb50d3b8104a6da6c8902cf0697cb748973
6
+ metadata.gz: 805e49cacca8b70246a6549c52a7e4bde532989ab80dcd375e0c238ed880aedf55a6f51ebc1275b030ad81daf6dae81753ffdb3590af14ba309383a7aa9473da
7
+ data.tar.gz: 5d7f808aaf2fa1b77338fbf8f4f236ec41bcf1c4b89113e9d55608a56ac349416a8a91e3fb951b387383b937cdd125c2c19816fad78191bfc0285245e41ef854
data/README.md CHANGED
@@ -1,21 +1,19 @@
1
1
  # RenderTurboStream
2
2
 
3
- Defining templates like `(create|update).turbo_stream.haml` annoyed me.
3
+ DHH made a milestone in web development with [Rails 7, "Fulfilling a Vision"](https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision). This gem is my contribution to that bold new step. The goal is to have a complete rails like workflow.
4
4
 
5
- Working consistently with turbo_stream or Turbo Streams Channel means shooting a lot of partials from the backend to the frontend. This always requires the same attributes: the path to the partial, the html id that turbo_stream points to, and maybe some locals, and maybe generate a translated flash message for update or create actions. All this can be done in one step directly from the controller.
5
+ Defining templates like `(create|update).turbo_stream.haml` annoyed me. Because it is a heavy mix of logic and view. Working consistently with turbo_stream or Turbo Streams Channel means shooting a lot of partials from the backend to the frontend. There are many ways and tedious details to handle redirects since Turbo! There is a lot that can be streamlined.
6
6
 
7
- There are many ways and tedious details to handle redirects since Turbo! Below described are options.
7
+ Execute [turbo_power](https://github.com/marcoroth/turbo_power) commands such as adding a css class to an html element, pushing a state to the browser history, or running custom javascript actions through Turbo Stream can be written in pure ruby code. No need for embeds like `.html.erb`.
8
8
 
9
- Turbo has seamlessly integrated Action Cable. The intention here is to bring it all together in a unified workflow with a sufficient testing strategy.
9
+ Logic to ruby, view to views and the area between for the gem! And with specific testings for the Ruby side.
10
10
 
11
- Execute [turbo_power](https://github.com/marcoroth/turbo_power) commands, such as adding a css class to an html element, pushing a state to the browser history, or running custom javascript actions through Turbo Stream (cable not yet integrated here) can be written in pure ruby code. No need for embeds like `.html.erb`.
12
-
13
- This plugin is in a early state.
11
+ This gem is in a early state.
14
12
 
15
13
  An overview of how we design a rails-7 application with turbo
16
14
  is [published on dev.to](https://dev.to/chmich/rails-7-vite-wrapping-up-1pia).
17
15
 
18
- A quick and dirty application with all the features, including tests, built in is [here](https://gitlab.com/sedl/renderturbostream_railsapp).
16
+ A quick and dirty application with all the features, including built in tests is [here](https://gitlab.com/sedl/renderturbostream_railsapp).
19
17
 
20
18
  Hope it can help you.
21
19
 
@@ -34,15 +32,16 @@ bundle install
34
32
  ApplicationController
35
33
 
36
34
  ```ruby
37
- include RenderTurboStream
35
+ include RenderTurboStream::ControllerHelpers
38
36
  ```
39
37
 
40
38
  spec/rails_helper.rb
41
39
 
42
40
  ```ruby
43
41
  RSpec.configure do |config|
44
- config.include RenderTurboStream::Test::RequestHelpers, type: :request
45
- config.include RenderTurboStream::Test::RspecRequestHelpers, type: :request #=> if you are on rspec
42
+ ...
43
+ config.include RenderTurboStream::Test::Request::Helpers, type: :request
44
+ config.include RenderTurboStream::Test::Request::ChannelHelpers, type: :request
46
45
  end
47
46
  ```
48
47
 
@@ -51,9 +50,10 @@ end
51
50
  Required Configurations for Flash Partial
52
51
 
53
52
  ```ruby
54
- config.x.render_turbo_stream.flash_partial = 'layouts/flash'
55
- config.x.render_turbo_stream.flash_id = 'flash-box'
56
- config.x.render_turbo_stream.flash_action = 'prepend'
53
+ config.x.render_turbo_stream.flash_partial = 'layouts/flash'
54
+ config.x.render_turbo_stream.flash_id = 'flash-box'
55
+ config.x.render_turbo_stream.flash_action = 'prepend'
56
+ config.x.render_turbo_stream.use_channel_for_turbo_stream_save = true
57
57
  ```
58
58
 
59
59
  The corresponding partials for flashes could look [like this](https://gitlab.com/sedl/renderturbostream/-/wikis/Flashes-example)
@@ -81,7 +81,8 @@ A comprehensive tutorial on turbo and how to check that it is working properly c
81
81
 
82
82
  **Turbo::StreamsChannel**
83
83
 
84
- The Rails team has seamlessly integrated `ActionCable` as `Turbo::StreamsChannel` into `Turbo Rails`. For installation along with this gem, see the [README](https://gitlab.com/sedl/renderturbostream/-/blob/main/README-cable.md).
84
+ The Rails team has integrated `ActionCable` as `Turbo::StreamsChannel` into `Turbo Rails`. For installation along with this gem, see the [README-channels](https://gitlab.com/sedl/renderturbostream/-/blob/main/README-channels.md).
85
+
85
86
  # Usage
86
87
 
87
88
  `turbo_stream_save` is a special method for streamlining `update` or `create` functions with `turbo_stream`. A controller action for update might look like this:
@@ -90,10 +91,9 @@ The Rails team has seamlessly integrated `ActionCable` as `Turbo::StreamsChannel
90
91
 
91
92
  def update
92
93
  turbo_stream_save(
93
- @customer.update(customer_params),
94
- turbo_redirect_on_success_to: edit_customer_path(@customer),
95
- id: 'customer-form',
96
- partial: 'form'
94
+ @article.update(article_params),
95
+ turbo_redirect_on_success_to: (params['no_redirection'] == 'true' ? nil : articles_path),
96
+ id: 'form'
97
97
  )
98
98
  end
99
99
  ```
@@ -120,9 +120,9 @@ The `stream_partial` method is for rendering a partial alone.
120
120
 
121
121
  ```ruby
122
122
  stream_partial(
123
- id,
123
+ 'flash-box',
124
124
  partial: nil, #=> default: id.gsub('-','_')
125
- locals: {},
125
+ locals: { title: 'my title' },
126
126
  action: nil #=> default: :replace
127
127
  )
128
128
  ```
@@ -158,7 +158,7 @@ Suppose you have a CRUD controller that should do all its actions inside a turbo
158
158
  There are two workarounds:
159
159
 
160
160
  ```ruby
161
- config.x.render_turbo_stream.use_cable_for_turbo_stream_save = true
161
+ config.x.render_turbo_stream.use_channel_for_turbo_stream_save = true
162
162
  ```
163
163
 
164
164
  With this config, the `turbo_stream_save` method will send flash messages through `Turbo::StreamsChannel` (if installed as described above) to a channel to the currently logged in user in parallel with the redirect.
@@ -213,81 +213,48 @@ def update
213
213
  flashes_on_error: [] #=> array of strings
214
214
 
215
215
 
216
- # Testing Scenarios
216
+ # Testing
217
217
 
218
218
  For system testing there is Capybara. Its the only way to check if frontend and backend work together. But its a good practice to break tests into smaller pieces. The much larger number of tests we will write on the much faster request tests, examples here in rspec.
219
219
 
220
+ **request tests cannot test javascript. they only test the ruby side: check if the right content is sent to turbo**.
221
+
220
222
  If the request format is not `turbo_stream`, which is the case on request specs, the method responds in a special html
221
223
  that contains the medadata that is interesting for our tests and is parsed by included test helpers.
222
224
 
223
- There is a helper for writing the test: In the debugger, within the test, check the output of `streams_log`.
225
+ There is a helper for writing the test: In the debugger, within the test, check the output of `all_turbo_responses`.
224
226
 
225
227
  Test scenarios are:
226
228
 
227
229
  **The fastest**
228
230
 
229
- If you want to check if a controller action succeeded, just check for `response.status`. The `turbo_stream_save` method returns three statuses: `200` if `save_action` is true, otherwise `422` and `302` for redirect. If one of the declared partials does not exist or breaks, the server will respond with exception anyway.
230
-
231
- **rspec**
232
-
233
- For rspec there is a special helper, for a successful save action:
234
-
235
- ```ruby
236
- it 'update failed' do
237
- patch article_path(article, params: valid_params)
238
- expect_successful_saved('customer-form', 'flash-box')
239
- # expects response.status 200
240
- # Make sure that the responses point to exactly these 2 IDs ('form' and 'flash-box').
241
- # Make sure that to each ID is responded to exactly once.
242
- end
243
- ```
231
+ If you want to check if a controller action succeeded, just check for `response.status`. The `turbo_stream_save` method returns three statuses: `200` if `save_action` is true, otherwise `422` and `302` for redirect. If one of the declared partials does not exist or breaks, the server will respond with exception anyway.
244
232
 
245
233
  **Redirection**
246
234
  ```ruby
247
235
  it 'update success' do
248
- patch article_path(article, params: valid_params)
249
- expect(turbo_redirect_to).to eq(articles_path)
250
- end
236
+ patch article_path(article, params: valid_params)
237
+ expect(turbo_redirect_to).to eq(articles_path)
238
+ end
251
239
  ```
252
240
 
253
- **Easier check what the partials have done**
241
+ **Test turbo stream response**
254
242
 
255
- For checking a little more inside the partial responses, but with a simple syntax that checks for a one-time response from a specific partial and may be sufficient for most cases:
256
243
  ```ruby
257
- expect(stream_targets_count).to eq(2)
244
+ expect(turbo_targets.length).to eq(2)
258
245
  # Check the total number of targeted html-ids, in most cases it will be one form and one flash.
259
-
260
- expect(stream_response('customer-form')).to eq(true)
261
- # Make sure that the id «customer-form» is targeted exactly once.
262
246
 
263
- expect(stream_response('customer-form', css: '.field_with_errors', include_string: 'Title')).to eq(true)
264
- # Verify that the response targeted to "customer-form" contains the specified html.
247
+ assert_stream_response('form'){|e|e.css('.field_with_errors').inner_html.include?('title')}
248
+ # make sure that there is one response to the target '#form' and it checks the rendered content
249
+ # if cero or more responses are expected, add a attribute like 'count: 0'
265
250
  ```
266
-
267
- **More detailed**
268
-
269
- Consider a controller action that should respond in 2 flashes:
251
+ Possible matchers can be found at [Nokogiri](https://nokogiri.org/tutorials/searching_a_xml_html_document.html).
270
252
 
271
253
  ```ruby
272
- expect(stream_targets_count).to eq(1)
273
-
274
- expect(
275
- stream_target_response_count('flash-box', total: 2) do |p|
276
- p.css('.callout.success').inner_html.include?('All perfect')
277
- end
278
- ).to eq(1)
279
-
280
- expect(
281
- stream_target_response_count('flash-box', total: 2) do |p|
282
- p.css('.callout.alert').inner_html.include?('Something went wrong')
283
- end
284
- ).to eq(1)
254
+ # check actions from turbo_power gem
255
+ assert_stream_response('colored-element', action: 'remove_css_class') {|args| args.last == 'background-red' }
256
+ # block (all within {}) is optional
285
257
  ```
286
- `stream_target_response_count` returns the number of matched responses.
287
-
288
- Possible matchers can be found at [Nokogiri](https://nokogiri.org/tutorials/searching_a_xml_html_document.html).
289
-
290
- For more detailed testing of partials, there are view tests.
291
258
 
292
259
  **P.S.:**
293
260
 
@@ -296,7 +263,7 @@ includes the plugin and has tests done by rspec/request and capybara.
296
263
 
297
264
  # More Configs
298
265
 
299
- On test helpers, the marker for a turbo-stream target is in most cases the id of the target element. This is true for the standard turbo-stream functions. On `turbo_power` it is the same in most cases. `RenderTurboStream::Libs.first_arg_is_html_id()` checks for which methods this is true. You can override this:
266
+ On test helpers, the marker for a turbo-stream target is in most cases the id of the target element. This is true for the standard turbo-stream functions. On `turbo_power` it is the same in most cases. `RenderTurboStream::Test::Request::Libs.first_arg_is_html_id()` checks for which methods this is true. You can override this:
300
267
 
301
268
  ```ruby
302
269
  config.x.render_turbo_stream.first_argument_is_html_id = %[replace append prepend turbo_frame_set_src]
@@ -0,0 +1 @@
1
+ <%= turbo_stream.send(command, *arguments) %>
@@ -4,9 +4,9 @@
4
4
  <% streams.each do |s| %>
5
5
 
6
6
  <% if s.is_a?(Array) %>
7
- <% attr_id = RenderTurboStream::Libs.first_arg_is_html_id(s.first) %>
8
- <% h = { attributes: s, type: 'command' } %>
9
- <% h[:target] = "##{s.second}" if attr_id %>
7
+ <% attr_id = RenderTurboStream::Test::Request::Libs.first_arg_is_html_id(s.first) %>
8
+ <% h = { array: s, type: 'stream-command', action: s.first } %>
9
+ <% h[:target] = "##{s.second[0]=='#' ? s.second[1..-1] : s.second}" if attr_id %>
10
10
  <% rendered_partials.push(h) %>
11
11
  <% else %>
12
12
  <% html = (render s[:partial], locals: s[:locals]&.symbolize_keys, formats: [:html]) %>
@@ -0,0 +1,94 @@
1
+ module RenderTurboStream
2
+ class ChannelLibs
3
+
4
+ def self.render_to_channel(response, channel, target_id, action, partial: nil, template: nil, locals: nil)
5
+
6
+ locals = {} unless locals # prevent error missing keys for nil
7
+
8
+ # add headers for test
9
+ if Rails.env.test?
10
+ html = RenderTurboStreamRenderController.render(partial: partial, locals: locals) if partial
11
+ html = RenderTurboStreamRenderController.render(template: template, locals: locals) if template
12
+ props = {
13
+ target: "##{target_id}",
14
+ action: action,
15
+ type: 'channel-partial',
16
+ locals: locals,
17
+ channel: channel,
18
+ html_response: html.to_s
19
+ }
20
+
21
+ props[:partial] = partial if partial
22
+ props[:template] = template if template
23
+ h = response.headers.to_h
24
+ i = 1
25
+ loop do
26
+ k = "test-turbo-channel-#{i}"
27
+ unless h.keys.include?(k)
28
+ response.headers[k] = props.to_json
29
+ break
30
+ end
31
+ i += 1
32
+ end
33
+ end
34
+
35
+ # send
36
+
37
+ if partial
38
+ Turbo::StreamsChannel.send(
39
+ "broadcast_#{action}_to",
40
+ channel.to_s,
41
+ target: target_id,
42
+ partial: partial,
43
+ locals: locals&.symbolize_keys,
44
+ layout: false
45
+ )
46
+ elsif template
47
+ Turbo::StreamsChannel.send(
48
+ "broadcast_#{action}_to",
49
+ channel.to_s,
50
+ target: target_id,
51
+ template: template,
52
+ locals: locals&.symbolize_keys,
53
+ layout: false
54
+ )
55
+ end
56
+ end
57
+
58
+ def self.action_to_channel(response, channel, command, arguments)
59
+
60
+ if Rails.env.test?
61
+
62
+ props = {
63
+ action: command,
64
+ type: 'channel-command',
65
+ channel: channel,
66
+ array: [command] + arguments,
67
+ }
68
+ if RenderTurboStream::Test::Request::Libs.first_arg_is_html_id(command)
69
+ target_id = (arguments.first[0..0] == '#' ? arguments.first[1..-1] : arguments.first)
70
+ props[:target] = "##{target_id}"
71
+ end
72
+
73
+ h = response.headers.to_h
74
+ i = 1
75
+ loop do
76
+ k = "test-turbo-channel-#{i}"
77
+ unless h.keys.include?(k)
78
+ response.headers[k] = props.to_json
79
+ break
80
+ end
81
+ i += 1
82
+ end
83
+ end
84
+
85
+ content = RenderTurboStreamRenderController.render(template: 'render_turbo_stream_command', layout: false, locals: { command: command, arguments: arguments })
86
+
87
+ Turbo::StreamsChannel.broadcast_stream_to(
88
+ channel,
89
+ content: content
90
+ )
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,36 @@
1
+ module RenderTurboStream
2
+ module ChannelViewHelpers
3
+
4
+ def channel_from_me
5
+ if current_user
6
+ turbo_stream_from "authenticated-user-#{current_user.id}"
7
+ else
8
+ Rails.logger.debug(" • SKIP channel_from_me because not authenticated")
9
+ nil
10
+ end
11
+ end
12
+
13
+ def channel_from_authenticated_group(group)
14
+ if current_user
15
+ turbo_stream_from "authenticated-group-#{group}"
16
+ else
17
+ Rails.logger.debug(" • SKIP channel_from_authenticated_group because not authenticated")
18
+ nil
19
+ end
20
+ end
21
+
22
+ def channel_from_all
23
+ turbo_stream_from "all"
24
+ end
25
+
26
+ def channel_from_group(group)
27
+ if group.present?
28
+ turbo_stream_from group
29
+ else
30
+ Rails.logger.debug(" • SKIP channel_from_group because no group given")
31
+ nil
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,121 @@
1
+ module RenderTurboStream
2
+ module ControllerChannelHelpers
3
+
4
+ def render_to_all(target_id, action = :replace, partial: nil, template: nil, locals: nil)
5
+ render_to_channel(
6
+ 'all',
7
+ target_id,
8
+ action,
9
+ partial: partial,
10
+ template: template,
11
+ locals: locals
12
+ )
13
+ end
14
+
15
+ def render_to_me(target_id, action = :replace, partial: nil, locals: nil)
16
+ begin
17
+ u_id = helpers.current_user&.id
18
+ unless u_id.present?
19
+ Rails.logger.debug(' • SKIP RenderTurboStream.render_to_me because current_user is nil')
20
+ return
21
+ end
22
+ rescue
23
+ Rails.logger.debug(' • ERROR RenderTurboStream.render_to_me because current_user is not available')
24
+ return
25
+ end
26
+
27
+ render_to_channel(
28
+ "authenticated-user-#{helpers.current_user.id}",
29
+ target_id,
30
+ action,
31
+ partial: partial,
32
+ locals: locals
33
+ )
34
+ end
35
+
36
+ def render_to_authenticated_group(group, target_id, action = :replace, partial: nil, locals: nil)
37
+ begin
38
+ u_id = helpers.current_user&.id
39
+ unless u_id.present?
40
+ Rails.logger.debug(' • SKIP RenderTurboStream.render_to_authenticated_group because current_user is nil')
41
+ return
42
+ end
43
+ rescue
44
+ Rails.logger.debug(' • ERROR RenderTurboStream.render_to_authenticated_group because current_user method is not available')
45
+ return
46
+ end
47
+
48
+ render_to_channel(
49
+ "authenticated-group-#{group}",
50
+ target_id,
51
+ action,
52
+ partial: partial,
53
+ locals: locals
54
+ )
55
+ end
56
+
57
+ def render_to_channel(channel, target_id, action, partial: nil, template: nil, locals: nil)
58
+
59
+ disable_default = false
60
+ if partial
61
+ template = nil
62
+ elsif template.present? && !template.to_s.include?('/')
63
+ template = [controller_path, template].join('/')
64
+ disable_default = true
65
+ else
66
+ template = [controller_path, action_name].join('/')
67
+ disable_default = true
68
+ end
69
+
70
+ if disable_default && !@render_cable_template_was_called
71
+ # make sure default render action returns nil
72
+ render template: 'render_turbo_stream_empty_template', layout: false unless @render_cable_template_was_called
73
+ @render_cable_template_was_called = true
74
+ end
75
+
76
+ RenderTurboStream::ChannelLibs.render_to_channel(response, channel, target_id, action, template: template, partial: partial, locals: locals)
77
+
78
+ end
79
+
80
+ def action_to_all(action, *arguments)
81
+ action_to_channel('all', action, arguments)
82
+ end
83
+
84
+ def action_to_me(action, *arguments)
85
+
86
+ begin
87
+ u_id = helpers.current_user&.id
88
+ unless u_id.present?
89
+ Rails.logger.debug(' • SKIP RenderTurboStream.action_to_me because current_user is nil')
90
+ return
91
+ end
92
+ rescue
93
+ Rails.logger.debug(' • ERROR RenderTurboStream.action_to_me because current_user is not available')
94
+ return
95
+ end
96
+
97
+ action_to_channel("authenticated-user-#{helpers.current_user.id}", action, arguments)
98
+ end
99
+
100
+ def action_to_authenticated_group(group, action, *arguments)
101
+
102
+ begin
103
+ u_id = helpers.current_user&.id
104
+ unless u_id.present?
105
+ Rails.logger.debug(' • SKIP RenderTurboStream.action_to_authenticated_group because current_user is nil')
106
+ return
107
+ end
108
+ rescue
109
+ Rails.logger.debug(' • ERROR RenderTurboStream.action_to_authenticated_group because current_user method is not available')
110
+ return
111
+ end
112
+
113
+ action_to_channel("authenticated-group-#{group}", action, arguments)
114
+ end
115
+
116
+ def action_to_channel(channel, command, arguments)
117
+ RenderTurboStream::ChannelLibs.action_to_channel(response, channel, command, arguments)
118
+ end
119
+
120
+ end
121
+ end