view_component_reflex 1.3.0 → 1.7.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: 30533287ce1445add7dba2ebed5013ed80e792de560fa9c930f3e13dcbd612d7
4
- data.tar.gz: bf84f573e602b373855848b2fbe61245967e4f16e7a420b03c1b6414aa1c8c73
3
+ metadata.gz: eb949fc8de60d18d54d72195cd04947ba24ef73db4091b2ddd0c2992287df7d0
4
+ data.tar.gz: c474384a6b9c073ba4efbf924067113971e67c4e8bdc0fbfa6462471ecf95b6e
5
5
  SHA512:
6
- metadata.gz: 47dc5ab24c0711778b07b8460207b1e532c944173d4df2cc8fe33447868b96a9043ad972b5a9672a0ea09ad323199b7cfa15327634199d3218e69e6c797f4196
7
- data.tar.gz: d9dbc21f77baf9c73c20352a5163c8af52317a0138393d4128cc34bb60e903227e62e28e0f0e0ad92f4fa10d4206f79d295c74582774b93fefffd14801fe5c44
6
+ metadata.gz: e0f4a365d380e55ba8deb5f567f35fa81b2aa8ab108cd2aca4f6e97041139e8e53f446f30db3ddcea0ee0fcb08e62b2f98feef14b83f5810c54ce705a6564716
7
+ data.tar.gz: df46044a3c7bddc9ab204ddb052fce04f0cecf190428043a3aa4ff2fb5a1b86d7d4e9524fa509ecbea51991641f8b4da9b8eaed2b1f4ede565c6cdb922a07724
data/README.md CHANGED
@@ -30,7 +30,7 @@ end
30
30
  # counter_component.html.erb
31
31
  <%= component_controller do %>
32
32
  <p><%= @count %></p>
33
- <button type="button" data-reflex="click->CounterComponentReflex#increment" data-key="<%= key %>">Click</button>
33
+ <%= reflex_tag :increment, :button, "Click" %>
34
34
  <% end %>
35
35
  ```
36
36
 
@@ -39,7 +39,7 @@ end
39
39
  In order to reconcile state to components in collections, you can specify a `collection_key` method that returns some
40
40
  value unique to that component.
41
41
 
42
- ```
42
+ ```ruby
43
43
  class TodoComponent < ViewComponentReflex::Component
44
44
  def initialize(todo:)
45
45
  @todo = todo
@@ -59,16 +59,132 @@ end
59
59
  If a new parameter is passed to the component during rendering, it is used instead of what's in state.
60
60
  If you're storing instances in state, you can use this to properly compare them.
61
61
 
62
+ ```ruby
63
+ def permit_parameter?(initial_param, new_param)
64
+ if new_param.instance_of? MyModel
65
+ new_param.id == @my_model.id
66
+ else
67
+ super
68
+ end
69
+ end
70
+ ```
71
+
62
72
  ### omitted_from_state
63
- Return an array of instance variables you want to omit from state. Useful if you have an object
64
- that isn't serializable as an instance variable, like a form.
73
+ Return an array of instance variables you want to omit from state. Only really useful if you're using the session state
74
+ adapter, and you have an instance variable that can't be serialized.
65
75
 
66
- ```
76
+ ```ruby
67
77
  def omitted_from_state
68
78
  [:@form]
69
79
  end
70
80
  ```
71
81
 
82
+ ### reflex_tag(reflex, name, content_or_options_with_block = nil, options = nil, escape = true, &block)
83
+ This shares the same definition as `content_tag`, except it accepts a reflex as the first parameter.
84
+
85
+ ```erb
86
+ <%= reflex_tag :increment, :button, "Click me!" %>
87
+ ```
88
+
89
+ Would add a click handler to the `increment` method on your component.
90
+
91
+ To use a non-click event, specific that with `->` notiation
92
+
93
+ ```erb
94
+ <%= reflex_tag "mouseenter->increment", :button, "Click me!" %>
95
+ ```
96
+
97
+ ### collection_key
98
+ If you're rendering a component as a collection with `MyComponent.with_collection(SomeCollection)`, you must define this method to return some unique value for the component.
99
+ This is used to reconcile state in the background.
100
+
101
+ ```ruby
102
+ def initialize
103
+ @my_model = MyModel.new
104
+ end
105
+
106
+ def collection_key
107
+ @my_model.id
108
+ end
109
+ ```
110
+
111
+ ### prevent_refresh!
112
+ By default, VCR will re-render your component after it executes your method. `revent_refresh!` prevents this from happening.
113
+
114
+ ```ruby
115
+ def my_method
116
+ prevent_refresh!
117
+ @foo = Lbar
118
+ end # the rendered page will not reflect this change
119
+ ```
120
+
121
+ ### refresh_all!
122
+ Refresh the entire body of the page
123
+
124
+ ```ruby
125
+ def do_some_global_action
126
+ prevent_refresh!
127
+ session[:model] = MyModel.new
128
+ refresh_all!
129
+ end
130
+ ```
131
+
132
+ ### key
133
+ This is a key unique to a particular component. It's used to reconcile state between renders, and should be passed as a data attribute whenever a reflex is called
134
+
135
+ ```erb
136
+ <button type="button" data-reflex="click->MyComponent#do_something" data-key="<%= key %>">Click me!</button>
137
+ ```
138
+
139
+ ### component_controller(options = {}, &blk)
140
+ This is a view helper to properly connect VCR to the component. It outputs `<div data-controller="my-controller" key=<%= key %></div>`
141
+ You *must* wrap your component in this for everything to work properly.
142
+
143
+ ```erb
144
+ <%= component_controller do %>
145
+ <p><%= @count %></p
146
+ <% end %>
147
+ ```
148
+
149
+ ## Common patterns
150
+ A lot of the time, you only need to update specific components when changing instance variables. For example, changing `@loading` might only need
151
+ to display a spinner somewhere on the page. You can define setters to implicitly render the appropriate pieces of dom whenever that variable is set
152
+
153
+ ```ruby
154
+ def initialize
155
+ @loading = false
156
+ end
157
+
158
+ def loading=(new_value)
159
+ @loading = new_value
160
+ refresh! '#loader'
161
+ end
162
+
163
+ def do_expensive_action
164
+ prevent_refresh!
165
+
166
+ self.loading = true
167
+ execute_it
168
+ self.loading = false
169
+ end
170
+ ```
171
+
172
+ ```erb
173
+ <%= component_controller do %>
174
+ <div id="loader">
175
+ <% if @loading %>
176
+ <p>Loading...</p>
177
+ <% end %>
178
+ </div>
179
+
180
+ <button type="button" data-reflex="click->MyComponent#do_expensive_action" data-key="<%= key %>">Click me!</button>
181
+ <% end
182
+ ```
183
+
184
+ ## State
185
+
186
+ By default, view_component_reflex stores component state in memory. You can optionally set the state adapter
187
+ to use the session by changing `config.state_adapter` to `ViewComponentReflex::StateAdapter::Session`
72
188
 
73
189
  ## Custom State Adapters
74
190
 
@@ -4,21 +4,53 @@ module ViewComponentReflex
4
4
  def init_stimulus_reflex
5
5
  klass = self
6
6
  @stimulus_reflex ||= Object.const_set(name + "Reflex", Class.new(StimulusReflex::Reflex) {
7
- def refresh!(primary_selector = "[data-controller~=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]", *selectors)
7
+ def refresh!(*selectors)
8
8
  save_state
9
- @channel.send :render_page_and_broadcast_morph, self, [primary_selector, *selectors], {
10
- "dataset" => element.dataset.to_h,
11
- "args" => [],
12
- "attrs" => element.attributes.to_h,
13
- "selectors" => ["body"],
14
- "target" => "#{self.class.name}##{method_name}",
15
- "url" => request.url,
16
- "permanent_attribute_name" => "data-reflex-permanent"
17
- }
9
+
10
+ # If the component has instance variables omitted from state,
11
+ # we can't render it to string from here because those instance
12
+ # variables will be missing. In that case, set the selectors to the
13
+ # default selector and manually morph the page
14
+ if selectors.empty? && !component.can_render_to_string?
15
+ selectors.push selector
16
+ end
17
+
18
+ # If we're just updating the component itself, we can
19
+ # directly render it instead of rendering the entire page again
20
+ if selectors.empty?
21
+ refresh_component!
22
+ else
23
+ @channel.send :render_page_and_broadcast_morph, self, selectors, {
24
+ "dataset" => element.dataset.to_h,
25
+ "args" => [],
26
+ "attrs" => element.attributes.to_h,
27
+ "selectors" => ["body"],
28
+ "target" => "#{self.class.name}##{method_name}",
29
+ "url" => request.url,
30
+ "permanent_attribute_name" => permanent_attribute_name
31
+ }
32
+ end
33
+ end
34
+
35
+ def refresh_component!
36
+ # The component can't figure out the key when we render from here
37
+ # Luckily we already know the key, so we can manually override it
38
+ component.tap do |k|
39
+ k.define_singleton_method(:key) do
40
+ element.dataset[:key]
41
+ end
42
+ end
43
+ html = controller.render_component_to_string(component)
44
+ document = Nokogiri::HTML(html)
45
+ morph selector, document.css("#{selector} > *").to_s
46
+ end
47
+
48
+ def selector
49
+ "[data-controller~=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]"
18
50
  end
19
51
 
20
52
  def refresh_all!
21
- refresh!("body")
53
+ morph :body, render_page(self)
22
54
  end
23
55
 
24
56
  # SR's delegate_call_to_reflex in channel.rb
@@ -32,18 +64,17 @@ module ViewComponentReflex
32
64
  !!name.to_proc
33
65
  end
34
66
 
35
- before_reflex do |a|
36
- a.send a.method_name
37
- throw :abort
38
- end
39
-
40
67
  def method_missing(name, *args)
41
68
  super unless respond_to_missing?(name)
42
69
  state.each do |k, v|
43
70
  component.instance_variable_set(k, v)
44
71
  end
45
72
  name.to_proc.call(component, *args)
46
- refresh!
73
+ refresh! unless @prevent_refresh
74
+ end
75
+
76
+ def prevent_refresh!
77
+ @prevent_refresh = true
47
78
  end
48
79
 
49
80
  define_method :component_class do
@@ -62,7 +93,7 @@ module ViewComponentReflex
62
93
  return @component if @component
63
94
  @component = component_class.allocate
64
95
  reflex = self
65
- exposed_methods = [:params, :request, :element, :refresh!, :refresh_all!, :stimulus_controller, :session]
96
+ exposed_methods = [:params, :request, :element, :refresh!, :refresh_all!, :stimulus_controller, :session, :prevent_refresh!]
66
97
  exposed_methods.each do |meth|
67
98
  @component.define_singleton_method(meth) do |*a|
68
99
  reflex.send(meth, *a)
@@ -81,7 +112,7 @@ module ViewComponentReflex
81
112
 
82
113
  def save_state
83
114
  new_state = {}
84
- component.instance_variables.each do |k|
115
+ component.safe_instance_variables.each do |k|
85
116
  new_state[k] = component.instance_variable_get(k)
86
117
  end
87
118
  set_state(new_state)
@@ -98,15 +129,27 @@ module ViewComponentReflex
98
129
  helpers.controller.instance_variable_get(:@stimulus_reflex)
99
130
  end
100
131
 
101
- def component_controller(opts = {}, &blk)
132
+ def can_render_to_string?
133
+ omitted_from_state.empty?
134
+ end
135
+
136
+ def component_controller(opts_or_tag = :div, opts = {}, &blk)
102
137
  self.class.init_stimulus_reflex
103
138
  init_key
104
- opts[:data] = {
139
+
140
+ tag = :div
141
+ if opts_or_tag.is_a? Hash
142
+ options = opts_or_tag
143
+ else
144
+ tag = opts_or_tag
145
+ options = opts
146
+ end
147
+ options[:data] = {
105
148
  controller: self.class.stimulus_controller,
106
149
  key: key,
107
- **(opts[:data] || {})
150
+ **(options[:data] || {})
108
151
  }
109
- content_tag :div, capture(&blk), opts
152
+ content_tag tag, capture(&blk), options
110
153
  end
111
154
 
112
155
  # key is required if you're using state
@@ -121,6 +164,24 @@ module ViewComponentReflex
121
164
  @key = key
122
165
  end
123
166
 
167
+ def reflex_tag(reflex, name, content_or_options_with_block = {}, options = {}, escape = true, &block)
168
+ action, method = reflex.to_s.split("->")
169
+ if method.nil?
170
+ method = action
171
+ action = "click"
172
+ end
173
+ data_attributes = {
174
+ reflex: "#{action}->#{self.class.name}##{method}",
175
+ key: key
176
+ }
177
+ if content_or_options_with_block.is_a?(Hash)
178
+ merge_data_attributes(content_or_options_with_block, data_attributes)
179
+ else
180
+ merge_data_attributes(options, data_attributes)
181
+ end
182
+ content_tag(name, content_or_options_with_block, options, escape, &block)
183
+ end
184
+
124
185
  def collection_key
125
186
  nil
126
187
  end
@@ -135,18 +196,10 @@ module ViewComponentReflex
135
196
 
136
197
  def key
137
198
  # initialize session state
138
- if !stimulus_reflex? || session[@key].nil?
139
- new_state = {}
140
-
141
- # this will almost certainly break
142
- blacklist = [
143
- :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
144
- :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
145
- :@helpers, :@controller, :@request, :@content
146
- ]
147
- instance_variables.reject { |k| blacklist.include?(k) }.each do |k|
148
- new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k)
149
- end
199
+ if !stimulus_reflex? || ViewComponentReflex::Engine.state_adapter.state(request, @key).empty?
200
+
201
+ new_state = create_safe_state
202
+
150
203
  ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
151
204
  ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state)
152
205
  else
@@ -159,5 +212,38 @@ module ViewComponentReflex
159
212
  end
160
213
  @key
161
214
  end
215
+
216
+ def safe_instance_variables
217
+ instance_variables - unsafe_instance_variables
218
+ end
219
+
220
+ private
221
+
222
+ def unsafe_instance_variables
223
+ [
224
+ :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
225
+ :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
226
+ :@helpers, :@controller, :@request, :@content, :@tag_builder
227
+ ]
228
+ end
229
+
230
+ def create_safe_state
231
+ new_state = {}
232
+
233
+ # this will almost certainly break
234
+ safe_instance_variables.each do |k|
235
+ new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k)
236
+ end
237
+ new_state
238
+ end
239
+
240
+ def merge_data_attributes(options, attributes)
241
+ data = options[:data]
242
+ if data.nil?
243
+ options[:data] = attributes
244
+ else
245
+ options[:data].merge! attributes
246
+ end
247
+ end
162
248
  end
163
249
  end
@@ -1,7 +1,8 @@
1
- require "view_component_reflex/state_adapter/session"
2
- require "view_component_reflex/engine"
3
- require 'stimulus_reflex'
4
-
5
- module ViewComponentReflex
6
- # Your code goes here...
7
- end
1
+ require "view_component_reflex/state_adapter/session"
2
+ require "view_component_reflex/state_adapter/memory"
3
+ require "view_component_reflex/engine"
4
+ require 'stimulus_reflex'
5
+
6
+ module ViewComponentReflex
7
+ # Your code goes here...
8
+ end
@@ -1,13 +1,13 @@
1
- module ViewComponentReflex
2
- class Engine < ::Rails::Engine
3
- class << self
4
- mattr_accessor :state_adapter
5
-
6
- self.state_adapter = StateAdapter::Session
7
- end
8
-
9
- def self.configure
10
- yield self if block_given?
11
- end
12
- end
13
- end
1
+ module ViewComponentReflex
2
+ class Engine < ::Rails::Engine
3
+ class << self
4
+ mattr_accessor :state_adapter
5
+
6
+ self.state_adapter = StateAdapter::Memory
7
+ end
8
+
9
+ def self.configure
10
+ yield self if block_given?
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ VIEW_COMPONENT_REFLEX_MEMORY_STATE = {}
2
+ module ViewComponentReflex
3
+ module StateAdapter
4
+ class Memory
5
+ def self.state(request, key)
6
+ VIEW_COMPONENT_REFLEX_MEMORY_STATE[request.session.id.to_s] ||= {}
7
+ VIEW_COMPONENT_REFLEX_MEMORY_STATE[request.session.id.to_s][key] ||= {}
8
+ end
9
+
10
+ def self.set_state(request, _, key, new_state)
11
+ new_state.each do |k, v|
12
+ state(request, key)[k] = v
13
+ end
14
+ end
15
+
16
+ def self.store_state(request, key, new_state = {})
17
+ VIEW_COMPONENT_REFLEX_MEMORY_STATE[request.session.id.to_s] ||= {}
18
+ VIEW_COMPONENT_REFLEX_MEMORY_STATE[request.session.id.to_s][key] ||= {}
19
+ new_state.each do |k, v|
20
+ VIEW_COMPONENT_REFLEX_MEMORY_STATE[request.session.id.to_s][key][k] = v
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
- module ViewComponentReflex
2
- VERSION = '1.3.0'
3
- end
1
+ module ViewComponentReflex
2
+ VERSION = '1.7.0'
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component_reflex
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua LeBlanc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-21 00:00:00.000000000 Z
11
+ date: 2020-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -71,6 +71,7 @@ files:
71
71
  - app/components/view_component_reflex/component.rb
72
72
  - lib/view_component_reflex.rb
73
73
  - lib/view_component_reflex/engine.rb
74
+ - lib/view_component_reflex/state_adapter/memory.rb
74
75
  - lib/view_component_reflex/state_adapter/session.rb
75
76
  - lib/view_component_reflex/version.rb
76
77
  homepage: https://github.com/joshleblanc/view_component_reflex