render_turbo_stream 2.1.1 → 3.0.0

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: ac522064ae87773819b4fdd059166e10a8bbb805de1dbbd020781f806ac6faeb
4
- data.tar.gz: a78259d9e2468305da273618929296735f3217592ed0ac2f0311e14b9c913eaf
3
+ metadata.gz: 733ef3518b8c95cb2f5162d0abb6d6278e653219bf5a99b502159fa52e884be8
4
+ data.tar.gz: b526a407a00b9dee9e5e543a48d80d14c073d87bf2397ea9bf2c0027a3366527
5
5
  SHA512:
6
- metadata.gz: b9e22fa2a8dbb9d7bb85d9fd19993ac6511c4cf9364c20da4b950b4bb368839c48f8c6ac93f31452592bc286f717d4c63107db7757731e339f1a37b871c7332a
7
- data.tar.gz: 5c13d3c56d66450c20920403cef30c18c18e0101ba0c3de86516d9db26d57edcd5c3a37866dc6e2c9052e0f2be34c2751486b29361dd93a5b1f54f5255660dc6
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