view_component_reflex 0.5.1 → 1.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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f7e9a4ab35ecb85fef158f336121ffd080f26d344bbf804abfc1fd85849b6734
|
4
|
+
data.tar.gz: 202ab52b2ad27cca0b823cb13d71b0389d6dba68e35e1f92da2ef601a2690125
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aeccacfe56978cc599fabdba202e645950cbf09a52c1a42bcdd12620018a2299a6858c0cda5c13ad644d0e78c766cffef55d46227874ed54d4944c0cef70f042
|
7
|
+
data.tar.gz: e5e7798417649a6233513a741f7c891e05abe15c941e2ade30bb7a9200b8c9e93039ba54a656cff05bf5a1dab886dd1dfc7bfcc4b42539853ce2cac70857c606
|
data/README.md
CHANGED
@@ -6,51 +6,32 @@ ViewComponentReflex allows you to write reflexes right in your view component co
|
|
6
6
|
|
7
7
|
You can add reflexes to your component by adding inheriting from `ViewComponentReflex::Component`.
|
8
8
|
|
9
|
-
To add a reflex to your component, use the `reflex` method.
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
reflex :my_cool_reflex do
|
13
|
-
# do stuff
|
14
|
-
refresh!
|
15
|
-
end
|
16
|
-
```
|
17
|
-
|
18
9
|
This will act as if you created a reflex with the method `my_cool_stuff`. To call this reflex, add `data-reflex="click->MyComponentReflex#my_cool_reflex"`, just like you're
|
19
10
|
using stimulus reflex.
|
20
11
|
|
21
|
-
|
22
|
-
|
23
|
-
In addition to calling reflexes, there is a rudimentary state system. You can initialize component-local state with `initialize_state(obj)`, where `obj` is a hash.
|
24
|
-
|
25
|
-
You can access state with the `state` helper. See the code below for an example. Calling `set_state` will set the state,
|
26
|
-
and also re-render your component.
|
27
|
-
|
28
|
-
If you're using state add `data-key="<%= key %>"` to any html element using a reflex. This
|
29
|
-
lets ViewComponentReflex keep track of which state belongs to which component.
|
30
|
-
|
12
|
+
ViewComponentReflex will maintain your component's instance variables between renders. You need to include `data-key=<%= key %>` on your root element, as well
|
13
|
+
as any element that stimulates a reflex. ViewComponent is inherently state-less, so the key is used to reconcile state to its respective component.
|
31
14
|
|
15
|
+
### Example
|
32
16
|
```ruby
|
33
17
|
# counter_component.rb
|
34
18
|
class CounterComponent < ViewComponentReflex::Component
|
35
|
-
|
36
19
|
def initialize
|
37
|
-
|
38
|
-
count: 0
|
39
|
-
})
|
20
|
+
@count = 0
|
40
21
|
end
|
41
22
|
|
42
|
-
|
43
|
-
|
23
|
+
def increment
|
24
|
+
@count += 1
|
44
25
|
end
|
45
26
|
end
|
46
27
|
```
|
47
28
|
|
48
29
|
```erb
|
49
30
|
# counter_component.html.erb
|
50
|
-
|
51
|
-
<p><%=
|
31
|
+
<%= component_controller do %>
|
32
|
+
<p><%= @count %></p>
|
52
33
|
<button type="button" data-reflex="click->CounterComponentReflex#increment" data-key="<%= key %>">Click</button>
|
53
|
-
|
34
|
+
<% end %>
|
54
35
|
```
|
55
36
|
|
56
37
|
## Custom State Adapters
|
@@ -76,36 +57,27 @@ class YourAdapter
|
|
76
57
|
end
|
77
58
|
|
78
59
|
##
|
60
|
+
# set_state is used to modify the state. It accepts a reflex, which gives you
|
61
|
+
# access to the request, as well as the controller and other useful objects.
|
62
|
+
#
|
79
63
|
# reflex - The reflex instance that's trying to set the state
|
80
64
|
# key - a unique string that identifies the component
|
81
65
|
# new_state - the new state to set
|
82
66
|
def self.set_state(reflex, key, new_state)
|
67
|
+
# update the state
|
83
68
|
end
|
84
69
|
|
85
70
|
|
86
71
|
##
|
72
|
+
# store_state is used to replace the state entirely. It only accepts
|
73
|
+
# a request object, rather than a reflex because it's called from the component's
|
74
|
+
# side with the component's instance variables.
|
75
|
+
#
|
87
76
|
# request - a rails request object
|
88
77
|
# key - a unique string that identifies the component instance
|
89
78
|
# new_state - a hash containing the component state
|
90
79
|
def self.store_state(request, key, new_state = {})
|
91
|
-
#
|
92
|
-
# this will be called twice, once with key, once with key_initial
|
93
|
-
# key_initial contains the initial, unmodified state.
|
94
|
-
# it should be used in reconcile_state to decide whether or not
|
95
|
-
# to re-initialize the state
|
96
|
-
end
|
97
|
-
|
98
|
-
##
|
99
|
-
# request - a rails request object
|
100
|
-
# key - a unique string that identifies the component instance
|
101
|
-
# new_state - a hash containing the component state
|
102
|
-
def self.reconcile_state(request, key, new_state)
|
103
|
-
# The passed state should always match the initial state of the component
|
104
|
-
# if it doesn't, we need to reset the state to the passed value.
|
105
|
-
#
|
106
|
-
# This handles cases where your initialize_state param computes some value that changes
|
107
|
-
# initialize_state({ transaction: @customer.transactions.first })
|
108
|
-
# if you delete the first transaction, that ^ is no longer valid. We need to update the state.
|
80
|
+
# replace the state
|
109
81
|
end
|
110
82
|
end
|
111
83
|
```
|
@@ -1,83 +1,141 @@
|
|
1
1
|
module ViewComponentReflex
|
2
2
|
class Component < ViewComponent::Base
|
3
3
|
class << self
|
4
|
-
def
|
5
|
-
stimulus_reflex.reflex(name, &blk)
|
6
|
-
end
|
7
|
-
|
8
|
-
def stimulus_reflex
|
4
|
+
def init_stimulus_reflex
|
9
5
|
klass = self
|
10
6
|
@stimulus_reflex ||= Object.const_set(name + "Reflex", Class.new(StimulusReflex::Reflex) {
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
def refresh!
|
16
|
-
@channel.send :render_page_and_broadcast_morph, self, ["[data-controller=\"#{stimulus_controller}\"]"], {
|
7
|
+
def refresh!(primary_selector = "[data-controller=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]", *selectors)
|
8
|
+
save_state
|
9
|
+
@channel.send :render_page_and_broadcast_morph, self, [primary_selector, *selectors], {
|
17
10
|
dataset: element.dataset.to_h,
|
18
11
|
args: [],
|
19
12
|
attrs: element.attributes.to_h,
|
20
|
-
selectors: [
|
13
|
+
selectors: ["body"],
|
21
14
|
target: "#{self.class.name}##{method_name}",
|
22
15
|
url: request.url,
|
23
16
|
permanentAttributeName: "data-reflex-permanent"
|
24
17
|
}
|
25
18
|
end
|
26
19
|
|
27
|
-
def
|
28
|
-
|
29
|
-
refresh!
|
20
|
+
def refresh_all!
|
21
|
+
refresh!("body")
|
30
22
|
end
|
31
23
|
|
32
|
-
|
33
|
-
|
24
|
+
# SR's delegate_call_to_reflex in channel.rb
|
25
|
+
# uses method to gather the method parameters, but since we're abusing
|
26
|
+
# method_missing here, that'll always fail
|
27
|
+
def method(name)
|
28
|
+
name.to_sym.to_proc
|
29
|
+
end
|
30
|
+
|
31
|
+
def respond_to_missing?(name, _ = false)
|
32
|
+
!!name.to_proc
|
33
|
+
end
|
34
|
+
|
35
|
+
before_reflex do |a|
|
36
|
+
a.send a.method_name
|
34
37
|
throw :abort
|
35
38
|
end
|
36
39
|
|
37
|
-
def
|
38
|
-
|
40
|
+
def method_missing(name, *args)
|
41
|
+
super unless respond_to_missing?(name)
|
42
|
+
state.each do |k, v|
|
43
|
+
component.instance_variable_set(k, v)
|
44
|
+
end
|
45
|
+
name.to_proc.call(component, *args)
|
46
|
+
refresh!
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method :component_class do
|
50
|
+
@component_class ||= klass
|
51
|
+
end
|
52
|
+
|
53
|
+
private :component_class
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def stimulus_controller
|
58
|
+
component_class.stimulus_controller
|
59
|
+
end
|
60
|
+
|
61
|
+
def component
|
62
|
+
return @component if @component
|
63
|
+
@component = component_class.allocate
|
64
|
+
reflex = self
|
65
|
+
exposed_methods = [:element, :refresh!, :refresh_all!, :stimulus_controller]
|
66
|
+
exposed_methods.each do |meth|
|
67
|
+
@component.define_singleton_method(meth) do |*a|
|
68
|
+
reflex.send(meth, *a)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@component
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_state(new_state = {})
|
75
|
+
ViewComponentReflex::Engine.state_adapter.set_state(self, element.dataset[:key], new_state)
|
39
76
|
end
|
40
77
|
|
41
|
-
|
42
|
-
|
78
|
+
def state
|
79
|
+
ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
|
43
80
|
end
|
44
81
|
|
45
|
-
|
46
|
-
|
47
|
-
|
82
|
+
def save_state
|
83
|
+
new_state = {}
|
84
|
+
component.instance_variables.each do |k|
|
85
|
+
new_state[k] = component.instance_variable_get(k)
|
48
86
|
end
|
87
|
+
set_state(new_state)
|
49
88
|
end
|
50
89
|
})
|
51
90
|
end
|
52
91
|
end
|
53
92
|
|
54
|
-
def
|
55
|
-
|
93
|
+
def self.stimulus_controller
|
94
|
+
name.chomp("Component").underscore.dasherize
|
56
95
|
end
|
57
96
|
|
58
97
|
def stimulus_reflex?
|
59
98
|
helpers.controller.instance_variable_get(:@stimulus_reflex)
|
60
99
|
end
|
61
100
|
|
101
|
+
def component_controller(&blk)
|
102
|
+
opts = {data: {controller: self.class.stimulus_controller, key: key}}
|
103
|
+
view_context.content_tag :div, capture(&blk), opts
|
104
|
+
end
|
105
|
+
|
62
106
|
# key is required if you're using state
|
63
107
|
# We can't initialize the session state in the initial method
|
64
108
|
# because it doesn't have a view_context yet
|
65
109
|
# This is the next best place to do it
|
66
110
|
def key
|
67
|
-
|
111
|
+
self.class.init_stimulus_reflex
|
112
|
+
# we want the erb file that renders the component. `caller` gives the file name,
|
113
|
+
# and line number, which should be unique. We hash it to make it a nice number
|
114
|
+
key = caller.select { |p| p.include? ".html.erb" }[1]&.hash.to_s
|
115
|
+
if @key.nil? || @key.empty?
|
116
|
+
@key = key
|
117
|
+
end
|
68
118
|
|
69
119
|
# initialize session state
|
70
120
|
if !stimulus_reflex? || session[@key].nil?
|
71
|
-
|
72
|
-
|
121
|
+
new_state = {}
|
122
|
+
|
123
|
+
# this will almost certainly break
|
124
|
+
blacklist = [
|
125
|
+
:@view_context, :@lookup_context, :@view_renderer, :@view_flow,
|
126
|
+
:@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
|
127
|
+
:@helpers, :@controller, :@request
|
128
|
+
]
|
129
|
+
instance_variables.reject { |k| blacklist.include?(k) }.each do |k|
|
130
|
+
new_state[k] = instance_variable_get(k)
|
131
|
+
end
|
132
|
+
ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
|
73
133
|
else
|
74
|
-
|
134
|
+
ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v|
|
135
|
+
instance_variable_set(k, v)
|
136
|
+
end
|
75
137
|
end
|
76
138
|
@key
|
77
139
|
end
|
78
|
-
|
79
|
-
def state
|
80
|
-
ViewComponentReflex::Engine.state_adapter.state(request, key)
|
81
|
-
end
|
82
140
|
end
|
83
141
|
end
|
@@ -19,20 +19,6 @@ module ViewComponentReflex
|
|
19
19
|
request.session[key][k] = v
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
23
|
-
# The passed state should always match the initial state of the component
|
24
|
-
# if it doesn't, we need to reset the state to the passed value.
|
25
|
-
#
|
26
|
-
# This handles cases where your initialize_state param computes some value that changes
|
27
|
-
# initialize_state({ transaction: @customer.transactions.first })
|
28
|
-
# if you delete the first transaction, that ^ is no longer valid. We need to update the state.
|
29
|
-
def self.reconcile_state(request, key, new_state)
|
30
|
-
request.session["#{key}_initial"].each do |k, v|
|
31
|
-
if new_state[k] != v
|
32
|
-
request.session[key][k] = new_state[k]
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
22
|
end
|
37
23
|
end
|
38
24
|
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: 0.
|
4
|
+
version: 1.0.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-
|
11
|
+
date: 2020-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|