render_turbo_stream 2.1.2 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
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