render_turbo_stream 3.0.4 → 4.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: 536c9b9d2d37a6767f7134dea0a9a5d19e268dc2b15dd1fec52b6d29b9db99b1
4
- data.tar.gz: 294c01ceb4e7370050350d49d85f68a9acc09ca435c70a945dcde9abf81108e0
3
+ metadata.gz: 525e9a802269d71e60be6cbc44fe62ebbe677a89fd49e048b1dbc11c6ae4d2b0
4
+ data.tar.gz: 5b47c7b9a7dcc49491e4ab20eb2637654b62e971afe0abdf481ec9210e97f145
5
5
  SHA512:
6
- metadata.gz: 63e368ad4d85f001693395a66057e1229297e7332a5d2ac8bdf785fa1ec7042511a4d2ee491394dd11ebf6ac2dace4091f70a4b62945f43ec23d002ac9b6d4d6
7
- data.tar.gz: 4c5c9758b9a063071219765a74d033f32d77fe4e3fd713ed95ec993334c71670d5694a36293a5acb8074a6e0ae6e8e677ca098cd1db78fb8fcc8e3c5eb52d014
6
+ metadata.gz: c532c3c9a5307547f260ff1feefea8e43b3cb09e42c43eacad1771c8a72b16a9843703024e1a58891a0498592c53dd296f2a6d7ef0dac72554c838c7c42d3915
7
+ data.tar.gz: f1c5cf8193d9c3214d60c492d73e7ebaf844b848dfa88a602ee235af9c0fbe90d757584938cf3b58d661d507b119791d4fe544968580654406c63abdfe4c1864
data/README.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # RenderTurboStream
2
2
 
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.
3
+ 2021 DHH annouced a great milestone in web development with [Rails 7, "Fulfilling a Vision"](https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision).
4
4
 
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. And there are many ways and tedious details to handle redirects since Turbo! This can all be streamlined.
5
+ TTo say it up front: If you want to have all the benefits of Turbo without disturbing the handy default Rails workflow, you have to use WebSockets, which are well integrated in Turbo, see [README Turbo::StreamsChannel](https://gitlab.com/sedl/renderturbostream/-/blob/main/README-channels.md). But the starting point is here. And I recommend that you download my [Quick-and-dirty test project](https://gitlab.com/sedl/renderturbostream_railsapp), set it up, see all the tests succeed, and then read through this README.
6
+
7
+ 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. This gem checks if a partial has the necessary frames, wraps it, and includes a helper to make instance variables work for Turbo Streams as well. This way, a simple as-usual partial is compatible for withh and without turbo, and can be sent in any way.
8
+
9
+ It has a testing strategy!
10
+
11
+ A lot of details for handling many different ways of redirect, that are possible and necessary since Turbo, are handled.
6
12
 
7
13
  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
14
 
@@ -11,10 +17,10 @@ Logic to Ruby, views to views, and the space in between for the gem! And all thi
11
17
  An overview of how we design a rails-7 application with turbo
12
18
  is [published on dev.to](https://dev.to/chmich/rails-7-vite-wrapping-up-1pia).
13
19
 
14
- A quick and dirty application with all the features, including built in tests is [here](https://gitlab.com/sedl/renderturbostream_railsapp).
15
-
16
20
  Hope it can help you.
17
21
 
22
+ April 2023 first released this gem is in a very early state. I am happy if others contribute for increase the quality of this new kind of working with rails 7.
23
+
18
24
  **Chris**
19
25
 
20
26
  ## Installation
@@ -49,24 +55,29 @@ Required Configurations for Flash Partial
49
55
 
50
56
  ```ruby
51
57
  config.x.render_turbo_stream.flash_partial = 'layouts/flash'
52
- config.x.render_turbo_stream.flash_id = 'flash-box'
53
- config.x.render_turbo_stream.flash_action = 'prepend'
54
- config.x.render_turbo_stream.use_channel_for_turbo_stream_save = true
58
+ config.x.render_turbo_stream.flash_target_id = 'flash-box'
59
+ config.x.render_turbo_stream.flash_turbo_action = 'prepend'
60
+ config.x.render_turbo_stream.allow_channel_to_me_for_turbo_stream_save = true
55
61
  ```
56
62
 
63
+ For the latter you have to setup channels, see below.
64
+
57
65
  The corresponding partials for flashes could look [like this](https://gitlab.com/sedl/renderturbostream/-/wikis/Flashes-example)
58
66
 
59
67
  **Translations**
60
68
 
61
69
  ```
62
- activerecord.success.successfully_created
63
- activerecord.success.successfully_updated
64
- activerecord.errors.messages.could_not_create
65
- activerecord.errors.messages.could_not_update
70
+ en:
71
+ activerecord:
72
+ success:
73
+ successfully_created: '%<model_name>s successfully created'
74
+ successfully_updated: '%<model_name>s successfully updated'
75
+ errors:
76
+ messages:
77
+ could_not_create: '%<model_name>s could not be created'
78
+ could_not_update: '%<model_name>s could not be updated'
66
79
  ```
67
80
 
68
- example value: `"%<model_name>s successfully created"`
69
-
70
81
  Model name translations, see: Rails Docs.
71
82
 
72
83
  **Turbo power**
@@ -90,20 +101,19 @@ The Rails team has integrated `ActionCable` as `Turbo::StreamsChannel` into `Tur
90
101
  def update
91
102
  turbo_stream_save(
92
103
  @article.update(article_params),
93
- turbo_redirect_on_success_to: (params['no_redirection'] == 'true' ? nil : articles_path),
94
- id: 'form'
104
+ if_success_turbo_redirect_to: articles_path,
105
+ target_id: 'customer-form'
95
106
  )
96
107
  end
97
108
  ```
98
-
99
- This will set a status, generate a flash message and perform `render_turbo_stream`, which would, if update succeeded result in something like this:
109
+ This will set a status, generate a flash message, and run `render_turbo_stream`. If it fails, it would result in something like this:
100
110
 
101
111
  ```ruby
102
112
  render_turbo_stream(
103
113
  [
104
114
  {
105
115
  id: 'customer-form',
106
- partial: 'customers/form'
116
+ partial: 'customers/customer_form'
107
117
  },
108
118
  {
109
119
  id: 'flash-wrapper',
@@ -114,6 +124,8 @@ render_turbo_stream(
114
124
  )
115
125
  ```
116
126
 
127
+ If the update succeeds, it will do a `redirect_to` action from turbo_power
128
+
117
129
  The `stream_partial` method is for rendering a partial alone.
118
130
 
119
131
  ```ruby
@@ -149,90 +161,35 @@ render_turbo_stream(
149
161
 
150
162
  Under the hood, inside a `*.turbo_stream.erb` template, it does the following: `= turbo_stream.send args.first, *(args[1..-1])`
151
163
 
152
- **WORKAROUNDS for redirects inside frame**
153
-
154
- Suppose you have a CRUD controller that should do all its actions inside a turbo frame. Classic redirect_to, together with a turbo_stream action on the same response, would raise «Render and/or redirect were called multiple times in this action». See [issue](https://gitlab.com/sedl/renderturbostream/-/issues/3).
155
-
156
- There are two workarounds:
157
-
158
- ```ruby
159
- config.x.render_turbo_stream.use_channel_for_turbo_stream_save = true
160
- ```
161
-
162
- 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.
164
+ **Config: allow_channel_to_me_for_turbo_stream_save**
163
165
 
164
- Another workaround is to use the src attribute of the parent frame. `turbo_frame_set_src` comes from turbo_power. The code might look like this:
166
+ Turbo streams rely on the controller response, which can only be executed once for a request. Other than Turbo::StreamsChannel, they cannot run in parallel with the standard responses like redirect_to.
165
167
 
166
- ```ruby
167
- def update
168
- turbo_stream_save(
169
- @car.update(car_params),
170
- streams_on_success: [
171
- [
172
- :turbo_frame_set_src,
173
- 'cars-box',
174
- cars_path
175
- ]
176
- ]
177
- )
178
- end
179
-
180
- ```
181
-
182
- **Parameters for turbo_stream_save**
183
-
184
- save_action,
185
- redirect_on_success_to: nil, # does a regular redirect. Works if you are inside a turbo_frame and just want to redirect inside that frame BUT CANNOT STREAM OTHERS ACTIONS ON THE SAME RESPONSE https://github.com/rails/rails/issues/48056
186
- turbo_redirect_on_success_to: nil, # does a full page redirect (break out of all frames by turbo_power redirect)
187
- object: nil, # object used in save_action, example: @customer
188
- id: nil, # if nil: no partial is rendered
189
- partial: nil, # example: 'customers/form' default: "#{controller_path}/#{id}"
190
- action: 'replace', # options: append, prepend
191
- locals: {}, # locals used by the partial
192
- streams_on_success: [
193
- {
194
- id: nil,
195
- partial: 'form',
196
- locals: {},
197
- action: 'replace'
198
- }
199
- ], # additional partials that should be rendered if save_action succeeded
200
- streams_on_error: [
201
- {
202
- id: nil,
203
- partial: 'form',
204
- locals: {},
205
- action: 'replace'
206
- }
207
- ], # additional partials that should be rendered if save_action failed
208
- add_flash_alerts: [], #=> array of strings
209
- add_flash_notices: [], #=> array of strings
210
- flashes_on_success: [], #=> array of strings
211
- flashes_on_error: [] #=> array of strings
168
+ If this config is set to true, Turbo::StreamsChannel is installed and a current user is logged in:
212
169
 
170
+ If an `if_success_redirect_to` argument is provided and the save action was successful, `turbo_stream_save` would send the partials by channel.
213
171
 
214
172
  # Testing
215
173
 
216
- 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.
217
-
218
- **request tests cannot test javascript. they only test the ruby side: check if the right content is sent to turbo**.
174
+ To test if the whole system works together, including javascript actions, so that finally a part reaches the surface, there is **Capybara** system testing. But it is a good practice to **break tests into smaller pieces**. So, there are helpers for **enabling the much faster request tests that are much easier to maintain**.
219
175
 
220
176
  If the request format is not `turbo_stream`, which is the case on request specs, the method responds in a special html
221
177
  that contains the medadata that is interesting for our tests and is parsed by included test helpers.
222
178
 
223
179
  There is a helper for writing the test: In the debugger, within the test, check the output of `all_turbo_responses`.
224
180
 
225
- Test scenarios are:
181
+ **Response Status**
226
182
 
227
- **The fastest**
228
-
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.
183
+ The `turbo_stream_save` method sets three statuses to the response: `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
184
 
231
185
  **Redirection**
186
+
187
+ If you defined the attribute `if_success_turbo_redirect_to` which uses the redirect_to function from `turbo_power` if installed:
188
+
232
189
  ```ruby
233
- it 'update success' do
190
+ it 'update success' do
234
191
  patch article_path(article, params: valid_params)
235
- expect(turbo_redirect_to).to eq(articles_path)
192
+ assert_turbo_redirect_to('/articles')
236
193
  end
237
194
  ```
238
195
 
@@ -242,25 +199,17 @@ end
242
199
  expect(turbo_targets.length).to eq(2)
243
200
  # Check the total number of targeted html-ids, in most cases it will be one form and one flash.
244
201
 
202
+ assert_stream_action('turbo_frame_set_src'){ |args| args == ["cars-box", "/cars"] }
203
+ # if the turbo_power gem is installed
204
+
245
205
  assert_stream_response('form'){|e|e.css('.field_with_errors').inner_html.include?('title')}
246
206
  # make sure that there is one response to the target '#form' and it checks the rendered content
247
207
  # if cero or more responses are expected, add a attribute like 'count: 0'
248
208
  ```
249
209
 
250
- Possible matchers can be found at [Nokogiri](https://nokogiri.org/tutorials/searching_a_xml_html_document.html).
251
-
252
- The `assert_stream_response`, like the other matchers, checks by default that the same target is not affected multiple times by the `:replace` action within the same response. This is defined in the underlying `RenderTurboStream::Test::Request::Libs.select_responses` method, which is also used for the `assert_channel_to_*` methods.
253
-
254
- ```ruby
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
258
- ```
259
-
260
- **P.S.:**
210
+ Possible matchers for checking html content can be found at [Nokogiri](https://nokogiri.org/tutorials/searching_a_xml_html_document.html).
261
211
 
262
- Testing the plugin itself: There is a [quick-and-dirty app](https://gitlab.com/sedl/renderturbostream_railsapp) which
263
- includes the plugin and has tests done by rspec/request and capybara.
212
+ The `assert_stream_response` checks by default that the same target is not affected multiple times by the `:replace` action within the same response. This is defined in the underlying `RenderTurboStream::Test::Request::Libs.select_responses` method, which is also used for the `assert_channel_to_*` methods.
264
213
 
265
214
  # More Configs
266
215
 
@@ -0,0 +1,6 @@
1
+ <%= turbo_frame_tag local_assigns[:render_turbo_stream_target_id] do %>
2
+ <%# locs = { value: 'hi' } %>
3
+ <%# locs2 = local_assigns.to_h %>
4
+ <%#= render local_assigns[:render_turbo_stream_partial], locals: { article: 'hi' } %>
5
+ <%= yield %>
6
+ <% end %>
@@ -12,12 +12,21 @@
12
12
 
13
13
 
14
14
  <% else %>
15
- <% ctl = { partial: args[:partial], id: args[:id], action: args[:action] } %>
16
- <% info = { id: args[:id], partial: args[:partial], locals: args[:locals] } %>
15
+ <% ctl = { partial: args[:partial], target_id: args[:target_id], action: args[:action] } %>
16
+ <% info = { target_id: args[:target_id], partial: args[:partial], locals: args[:locals] } %>
17
+
18
+
19
+
17
20
  <% if args[:action].present? %>
18
21
  <% Rails.logger.debug(" • render-turbo-stream #{args[:action].upcase} => #{info}") %>
19
- <%= turbo_stream.send args[:action].to_sym, args[:id] do %>
20
- <%= render args[:partial], locals: args[:locals]&.symbolize_keys %>
22
+ <%= turbo_stream.send args[:action].to_sym, RenderTurboStream::Libs.target_to_target_id(args[:target]) do %>
23
+ <% if RenderTurboStream::CheckTemplate.new(partial: args[:partial], action: args[:action]).add_turbo_frame_tag? %>
24
+ <%= turbo_frame_tag RenderTurboStream::Libs.target_to_target_id(args[:target]) do %>
25
+ <%= render args[:partial], locals: args[:locals]&.symbolize_keys %>
26
+ <% end %>
27
+ <% else %>
28
+ <%= render args[:partial], locals: args[:locals]&.symbolize_keys %>
29
+ <% end %>
21
30
  <% end %>
22
31
 
23
32
  <% else %>
@@ -6,14 +6,22 @@
6
6
  <% if s.is_a?(Array) %>
7
7
  <% attr_id = RenderTurboStream::Test::Request::Libs.first_arg_is_html_id(s.first) %>
8
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 %>
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
- <% html = (render s[:partial], locals: s[:locals]&.symbolize_keys, formats: [:html]) %>
13
- <% s.delete(:partial) %>
14
- <% s[:target] = "##{s[:id]}" %>
12
+
13
+ <% wrap = RenderTurboStream::CheckTemplate.new(partial: s[:partial], action: s[:action]).add_turbo_frame_tag? %>
14
+
15
+ <% if wrap %>
16
+ <% html = turbo_frame_tag RenderTurboStream::Libs.target_to_target_id(s[:target]) do %>
17
+ <%= (render s[:partial], locals: s[:locals]&.symbolize_keys, formats: [:html]) %>
18
+ <% end %>
19
+ <% else %>
20
+ <% html = (render s[:partial], locals: s[:locals]&.symbolize_keys, formats: [:html]) %>
21
+ <% end %>
22
+ <% s[:target] = s[:target] %>
15
23
  <% s.delete(:id) %>
16
- <% rendered_partials.push({ html_response: html, type: 'partial' }.merge(s)) %>
24
+ <% rendered_partials.push({ html_response: html, type: 'stream-partial' }.merge(s)) %>
17
25
  <% end %>
18
26
 
19
27
 
@@ -1,16 +1,48 @@
1
1
  module RenderTurboStream
2
2
  class ChannelLibs
3
3
 
4
- def self.render_to_channel(response, channel, target_id, action, partial: nil, template: nil, locals: nil)
4
+ def initialize(response)
5
+ @response = response
6
+ end
7
+
8
+ def render_to_channel(channel, target, action, controller_self, partial: nil, template: nil, locals: nil)
9
+
10
+ # instance variables
11
+
12
+ target_id = RenderTurboStream::Libs.target_to_target_id(target)
13
+ check_template = RenderTurboStream::CheckTemplate.new(
14
+ partial: partial,
15
+ template: template,
16
+ available_instance_variables: controller_self.instance_variables,
17
+ action: action
18
+ )
19
+
20
+ # add instance_variables to locals
5
21
 
6
22
  locals = {} unless locals # prevent error missing keys for nil
23
+ if check_template.add_turbo_frame_tag?
24
+ _layout = 'layouts/add_turbo_frame_tag'
25
+ _locals = locals.merge(
26
+ {
27
+ render_turbo_stream_partial: partial,
28
+ render_turbo_stream_target_id: target_id
29
+ }
30
+ )
31
+ else
32
+ _locals = locals
33
+ _layout = false
34
+ end
35
+ check_template.templates_instance_variables.each do |v|
36
+ _locals[:"render_turbo_stream_instance_variable_#{v}"] = controller_self.instance_variable_get(v.to_s)
37
+ end
7
38
 
8
39
  # add headers for test
40
+
9
41
  if Rails.env.test?
10
- html = RenderTurboStreamRenderController.render(partial: partial, locals: locals) if partial
11
- html = RenderTurboStreamRenderController.render(template: template, locals: locals) if template
42
+ html = RenderTurboStreamRenderController.render(partial: partial, locals: _locals, layout: _layout) if partial
43
+ html = RenderTurboStreamRenderController.render(template: template, locals: _locals, layout: _layout) if template
12
44
  props = {
13
- target: "##{target_id}",
45
+ target: target,
14
46
  action: action,
15
47
  type: 'channel-partial',
16
48
  locals: locals,
@@ -20,12 +52,12 @@ module RenderTurboStream
20
52
 
21
53
  props[:partial] = partial if partial
22
54
  props[:template] = template if template
23
- h = response.headers.to_h
55
+ h = @response.headers.to_h
24
56
  i = 1
25
57
  loop do
26
58
  k = "test-turbo-channel-#{i}"
27
59
  unless h.keys.include?(k)
28
- response.headers[k] = props.to_json
60
+ @response.headers[k] = props.to_json
29
61
  break
30
62
  end
31
63
  i += 1
@@ -40,8 +72,8 @@ module RenderTurboStream
40
72
  channel.to_s,
41
73
  target: target_id,
42
74
  partial: partial,
43
- locals: locals&.symbolize_keys,
44
- layout: false
75
+ locals: _locals&.symbolize_keys,
76
+ layout: _layout
45
77
  )
46
78
  elsif template
47
79
  Turbo::StreamsChannel.send(
@@ -50,12 +82,12 @@ module RenderTurboStream
50
82
  target: target_id,
51
83
  template: template,
52
84
  locals: locals&.symbolize_keys,
53
- layout: false
85
+ layout: _layout
54
86
  )
55
87
  end
56
88
  end
57
89
 
58
- def self.action_to_channel(response, channel, command, arguments)
90
+ def action_to_channel(channel, command, arguments)
59
91
 
60
92
  if Rails.env.test?
61
93
 
@@ -67,15 +99,15 @@ module RenderTurboStream
67
99
  }
68
100
  if RenderTurboStream::Test::Request::Libs.first_arg_is_html_id(command)
69
101
  target_id = (arguments.first[0..0] == '#' ? arguments.first[1..-1] : arguments.first)
70
- props[:target] = "##{target_id}"
102
+ props[:target] = RenderTurboStream::Libs.target_id_to_target(target_id)
71
103
  end
72
-
73
- h = response.headers.to_h
104
+
105
+ h = @response.headers.to_h
74
106
  i = 1
75
107
  loop do
76
108
  k = "test-turbo-channel-#{i}"
77
109
  unless h.keys.include?(k)
78
- response.headers[k] = props.to_json
110
+ @response.headers[k] = props.to_json
79
111
  break
80
112
  end
81
113
  i += 1
@@ -90,5 +122,17 @@ module RenderTurboStream
90
122
  )
91
123
  end
92
124
 
125
+ def send_actions_to_channel(channel, actions, instance_variables)
126
+ actions.each do |a|
127
+ if a.is_a?(Array)
128
+ action_to_channel(channel, a.first, a[1..-1])
129
+ else
130
+ render_to_channel(channel, a[:target], a[:action], instance_variables, partial: a[:partial], template: a[:template], locals: a[:locals])
131
+ end
132
+ end
133
+ end
134
+
135
+ def self.fetch_partials_variables(relative_path) end
136
+
93
137
  end
94
138
  end
@@ -32,5 +32,13 @@ module RenderTurboStream
32
32
  end
33
33
  end
34
34
 
35
+ def fetch_instance_variables(locals)
36
+ locals.each do |k,v|
37
+ if k[0..38] == 'render_turbo_stream_instance_variable_@'
38
+ eval "#{k[38..-1]} = v"
39
+ end
40
+ end
41
+ end
42
+
35
43
  end
36
44
  end
@@ -0,0 +1,122 @@
1
+ module RenderTurboStream
2
+ class CheckTemplate
3
+
4
+ AUTO_ADD_TURBO_FRAME_TAG_ON_ACTION = [:replace]
5
+
6
+ def initialize(partial: nil, template: nil, available_instance_variables: nil, action: nil)
7
+ if !partial && !template
8
+ raise 'missing attribute partial xor template'
9
+ end
10
+ unless action
11
+ raise 'missing required attribute: action'
12
+ end
13
+ @action = action
14
+ @partial_path = partial
15
+ @template_path = template
16
+ @available_instance_variables = available_instance_variables
17
+ prt = (partial ? partial : template).split('/')
18
+ if prt.length < 2
19
+ raise 'Partial or template path must always be specified with the controller path, for example «articles/partial_name».'
20
+ end
21
+ @controller_path = prt[0..-2].join('/')
22
+ @key = relative_path(partial: partial, template: template)
23
+ if production?
24
+ $render_turbo_stream_check_templates ||= {}
25
+ if $render_turbo_stream_check_templates[@key]
26
+ @result = $render_turbo_stream_check_templates[@key]
27
+ else
28
+ @result = { instance_variables: [], add_turbo_frame_tag: false }
29
+ fetch_nested_partials(1, partial: @partial_path, template: @template_path)
30
+ $render_turbo_stream_check_templates[@key] ||= @result
31
+ end
32
+ else
33
+ @result = { instance_variables: [], add_turbo_frame_tag: false }
34
+ fetch_nested_partials(1, partial: @partial_path, template: @template_path)
35
+ end
36
+ end
37
+
38
+ def templates_instance_variables
39
+ @result[:instance_variables]
40
+ end
41
+
42
+ def add_turbo_frame_tag?
43
+ @result[:add_turbo_frame_tag]
44
+ end
45
+
46
+ private
47
+
48
+ def production?
49
+ # in production this config should be set to true
50
+ !Rails.application.config.assets.compile
51
+ end
52
+
53
+ def fetch_nested_partials(stack_level, partial: nil, template: nil)
54
+
55
+ @nested_partials_done ||= []
56
+ if @nested_partials_done.include?(partial || template)
57
+ return # prevent endless loops
58
+ end
59
+ @nested_partials_done.push(partial || template)
60
+ absolute_path = absolute_path(partial: partial, template: template)
61
+ code = File.read(absolute_path)
62
+
63
+ _code = code.dup
64
+ _code.scan(/@[a-z_]+/).each do |var|
65
+ if !@available_instance_variables || (@available_instance_variables && @available_instance_variables.include?(var.to_sym))
66
+ unless @result[:instance_variables].include?(var.to_sym)
67
+ @result[:instance_variables].push(var.to_sym)
68
+ end
69
+ end
70
+ end
71
+
72
+ if stack_level == 1 && AUTO_ADD_TURBO_FRAME_TAG_ON_ACTION.include?(@action.to_sym)
73
+ unless _code.match(/(turbo_frame_tag|turbo-frame)/)
74
+ @result[:add_turbo_frame_tag] = true
75
+ end
76
+ end
77
+
78
+ loop do
79
+ _matched_partial = _code.match((/\brender[ ](partial:|())(|[ ])("|')[a-z_\/]+("|')/))
80
+ _code = _code[(_code.index(_matched_partial.to_s) + 5)..-1]
81
+ if _matched_partial
82
+
83
+ found_partial = _matched_partial.to_s.split(/("|')/)[-2]
84
+ absolute_path = absolute_path(partial: found_partial)
85
+
86
+ @nested_partials ||= []
87
+ @nested_partials.push(absolute_path)
88
+ fetch_nested_partials(stack_level += 1, partial: found_partial)
89
+
90
+ else
91
+ break
92
+ end
93
+ end
94
+ renders = code.match((/\brender[ ](partial:|())(|[ ])("|')[a-z_\/]+("|')/)) # scan function didnt work here, so built a loop
95
+ end
96
+
97
+ def absolute_path(partial: nil, template: nil)
98
+ divided = relative_path(partial: partial, template: template).split('/')
99
+ view_folder = divided[0..-2].join('/')
100
+ folder = Rails.root.join('app', 'views', view_folder)
101
+ items = Dir.glob "#{folder.to_s}/*"
102
+ item = items.select { |i| i.split('/').last.match(/^#{divided.last}/) }.first
103
+ unless item.present?
104
+ raise "#{partial ? 'Partial' : 'Template'} not found => #{relative_path(partial: partial, template: template)}"
105
+ end
106
+ item
107
+ end
108
+
109
+ def relative_path(partial: nil, template: nil)
110
+ divided = (partial ? partial : template).split('/')
111
+ if divided.length <= 1
112
+ _divided = [@controller_path] + divided
113
+ else
114
+ _divided = divided
115
+ end
116
+ view_folder = _divided[0..-2].join('/')
117
+ file = "#{partial ? '_' : ''}#{divided.last}"
118
+ [view_folder, file].join('/')
119
+ end
120
+
121
+ end
122
+ end