view_component_reflex 1.3.0 → 1.7.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 +4 -4
- data/README.md +121 -5
- data/app/components/view_component_reflex/component.rb +121 -35
- data/lib/view_component_reflex.rb +8 -7
- data/lib/view_component_reflex/engine.rb +13 -13
- data/lib/view_component_reflex/state_adapter/memory.rb +25 -0
- data/lib/view_component_reflex/version.rb +3 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb949fc8de60d18d54d72195cd04947ba24ef73db4091b2ddd0c2992287df7d0
|
4
|
+
data.tar.gz: c474384a6b9c073ba4efbf924067113971e67c4e8bdc0fbfa6462471ecf95b6e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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.
|
64
|
-
|
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!(
|
7
|
+
def refresh!(*selectors)
|
8
8
|
save_state
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
-
**(
|
150
|
+
**(options[:data] || {})
|
108
151
|
}
|
109
|
-
content_tag
|
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? ||
|
139
|
-
|
140
|
-
|
141
|
-
|
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/
|
3
|
-
require
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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::
|
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
|
-
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.
|
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-
|
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
|