render_turbo_stream 2.1.2 → 3.0.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: 92144d161ab6a0eee72442b7b848a140b9f439cbda17a3e41ad4a28aa5f0a8cb
4
- data.tar.gz: 3b290864b96789968ab8ad704b1127cb37ab6c00985c11c1cd87c28a2cb87695
3
+ metadata.gz: 733ef3518b8c95cb2f5162d0abb6d6278e653219bf5a99b502159fa52e884be8
4
+ data.tar.gz: b526a407a00b9dee9e5e543a48d80d14c073d87bf2397ea9bf2c0027a3366527
5
5
  SHA512:
6
- metadata.gz: ea284231a3dd9343643a868bde136c954e32d0347354c5127f83b64e297e81309372214490df90580f32143c9a0f1422fe8f6abb2e2e1b9f7096726fb1454896
7
- data.tar.gz: c8719357d38e4ced162e9bc2b94401ee3e7096f6097e1402b9ac9670c3b124326da0a9ac858b9ff5a434458efd010fb50d3b8104a6da6c8902cf0697cb748973
6
+ metadata.gz: edf51ffaf2a192f7eb65f6147ba3d2b64cbb07d0c444a4bc00ec2d7a145cceaac943704ec1b50199e0ac1b4f42e3f524386679d682af728ca0f577b865dc72d9
7
+ data.tar.gz: '09e87e2f74275a48a459d47dac942c66787e2127d9cf61e08ff16a3e7dcf5cc63708d4c1b0d4d7fba89a429a0ff76f5e95a491906650ba167bf27bde82784afd'
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,17 @@ bundle install
34
32
  ApplicationController
35
33
 
36
34
  ```ruby
37
- include RenderTurboStream
35
+ include RenderTurboStream::ControllerHelpers
36
+ include RenderTurboStream::ControllerChannelHelpers
38
37
  ```
39
38
 
40
39
  spec/rails_helper.rb
41
40
 
42
41
  ```ruby
43
42
  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
43
+ ...
44
+ config.include RenderTurboStream::Test::Request::Helpers, type: :request
45
+ config.include RenderTurboStream::Test::Request::ChannelHelpers, type: :request
46
46
  end
47
47
  ```
48
48
 
@@ -51,9 +51,10 @@ end
51
51
  Required Configurations for Flash Partial
52
52
 
53
53
  ```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'
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'
57
+ config.x.render_turbo_stream.use_cable_for_turbo_stream_save = true
57
58
  ```
58
59
 
59
60
  The corresponding partials for flashes could look [like this](https://gitlab.com/sedl/renderturbostream/-/wikis/Flashes-example)
@@ -81,7 +82,8 @@ A comprehensive tutorial on turbo and how to check that it is working properly c
81
82
 
82
83
  **Turbo::StreamsChannel**
83
84
 
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).
85
+ The Rails team has 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-channels.md).
86
+
85
87
  # Usage
86
88
 
87
89
  `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 +92,9 @@ The Rails team has seamlessly integrated `ActionCable` as `Turbo::StreamsChannel
90
92
 
91
93
  def update
92
94
  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'
95
+ @article.update(article_params),
96
+ turbo_redirect_on_success_to: (params['no_redirection'] == 'true' ? nil : articles_path),
97
+ id: 'form'
97
98
  )
98
99
  end
99
100
  ```
@@ -120,9 +121,9 @@ The `stream_partial` method is for rendering a partial alone.
120
121
 
121
122
  ```ruby
122
123
  stream_partial(
123
- id,
124
+ 'flash-box',
124
125
  partial: nil, #=> default: id.gsub('-','_')
125
- locals: {},
126
+ locals: { title: 'my title' },
126
127
  action: nil #=> default: :replace
127
128
  )
128
129
  ```
@@ -213,81 +214,48 @@ def update
213
214
  flashes_on_error: [] #=> array of strings
214
215
 
215
216
 
216
- # Testing Scenarios
217
+ # Testing
217
218
 
218
219
  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
220
 
221
+ **request tests cannot test javascript. they only test the ruby side: check if the right content is sent to turbo**.
222
+
220
223
  If the request format is not `turbo_stream`, which is the case on request specs, the method responds in a special html
221
224
  that contains the medadata that is interesting for our tests and is parsed by included test helpers.
222
225
 
223
- There is a helper for writing the test: In the debugger, within the test, check the output of `streams_log`.
226
+ There is a helper for writing the test: In the debugger, within the test, check the output of `all_turbo_responses`.
224
227
 
225
228
  Test scenarios are:
226
229
 
227
230
  **The fastest**
228
231
 
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
- ```
232
+ 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
233
 
245
234
  **Redirection**
246
235
  ```ruby
247
236
  it 'update success' do
248
- patch article_path(article, params: valid_params)
249
- expect(turbo_redirect_to).to eq(articles_path)
250
- end
237
+ patch article_path(article, params: valid_params)
238
+ expect(turbo_redirect_to).to eq(articles_path)
239
+ end
251
240
  ```
252
241
 
253
- **Easier check what the partials have done**
242
+ **Test turbo stream response**
254
243
 
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
244
  ```ruby
257
- expect(stream_targets_count).to eq(2)
245
+ expect(turbo_targets.length).to eq(2)
258
246
  # 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
247
 
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.
248
+ assert_stream_response('form'){|e|e.css('.field_with_errors').inner_html.include?('title')}
249
+ # make sure that there is one response to the target '#form' and it checks the rendered content
250
+ # if cero or more responses are expected, add a attribute like 'count: 0'
265
251
  ```
266
-
267
- **More detailed**
268
-
269
- Consider a controller action that should respond in 2 flashes:
252
+ Possible matchers can be found at [Nokogiri](https://nokogiri.org/tutorials/searching_a_xml_html_document.html).
270
253
 
271
254
  ```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)
255
+ # check actions from turbo_power gem
256
+ assert_stream_response('colored-element', action: 'remove_css_class') {|args| args.last == 'background-red' }
257
+ # block (all within {}) is optional
285
258
  ```
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
259
 
292
260
  **P.S.:**
293
261
 
@@ -296,7 +264,7 @@ includes the plugin and has tests done by rspec/request and capybara.
296
264
 
297
265
  # More Configs
298
266
 
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:
267
+ 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
268
 
301
269
  ```ruby
302
270
  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,93 @@
1
+ module RenderTurboStream
2
+ class ChannelLibs
3
+ def self.render_to_channel(response, channel, target_id, action, partial: nil, template: nil, locals: nil)
4
+
5
+ locals = {} unless locals # prevent error missing keys for nil
6
+
7
+ # add headers for test
8
+ if Rails.env.test?
9
+ html = RenderTurboStreamRenderController.render(partial: partial, locals: locals) if partial
10
+ html = RenderTurboStreamRenderController.render(template: template, locals: locals) if template
11
+ props = {
12
+ target: "##{target_id}",
13
+ action: action,
14
+ type: 'channel-partial',
15
+ locals: locals,
16
+ channel: channel,
17
+ html_response: html.to_s
18
+ }
19
+
20
+ props[:partial] = partial if partial
21
+ props[:template] = template if template
22
+ h = response.headers.to_h
23
+ i = 1
24
+ loop do
25
+ k = "test-turbo-channel-#{i}"
26
+ unless h.keys.include?(k)
27
+ response.headers[k] = props.to_json
28
+ break
29
+ end
30
+ i += 1
31
+ end
32
+ end
33
+
34
+ # send
35
+
36
+ if partial
37
+ Turbo::StreamsChannel.send(
38
+ "broadcast_#{action}_to",
39
+ channel.to_s,
40
+ target: target_id,
41
+ partial: partial,
42
+ locals: locals&.symbolize_keys,
43
+ layout: false
44
+ )
45
+ elsif template
46
+ Turbo::StreamsChannel.send(
47
+ "broadcast_#{action}_to",
48
+ channel.to_s,
49
+ target: target_id,
50
+ template: template,
51
+ locals: locals&.symbolize_keys,
52
+ layout: false
53
+ )
54
+ end
55
+ end
56
+
57
+ def self.action_to_channel(response, channel, command, arguments)
58
+
59
+ if Rails.env.test?
60
+
61
+ props = {
62
+ action: command,
63
+ type: 'channel-command',
64
+ channel: channel,
65
+ array: [command] + arguments,
66
+ }
67
+ if RenderTurboStream::Test::Request::Libs.first_arg_is_html_id(command)
68
+ target_id = (arguments.first[0..0] == '#' ? arguments.first[1..-1] : arguments.first)
69
+ props[:target] = "##{target_id}"
70
+ end
71
+
72
+ h = response.headers.to_h
73
+ i = 1
74
+ loop do
75
+ k = "test-turbo-channel-#{i}"
76
+ unless h.keys.include?(k)
77
+ response.headers[k] = props.to_json
78
+ break
79
+ end
80
+ i += 1
81
+ end
82
+ end
83
+
84
+ content = RenderTurboStreamRenderController.render(template: 'render_turbo_stream_command', layout: false, locals: { command: command, arguments: arguments })
85
+
86
+ Turbo::StreamsChannel.broadcast_stream_to(
87
+ channel,
88
+ content: content
89
+ )
90
+ end
91
+
92
+ end
93
+ 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