view_component_reflex 0.6.1 → 1.1.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: a9049a220598d64907ca4d9a7b77dc9df6e8913070ced348aa5182b666c35395
4
- data.tar.gz: 1521d4623ee081f03d12e8ce40dc2cdbca67f5475a63351d84541a5a3955c1c8
3
+ metadata.gz: 42797141d9ed8ecfb013f8f68c9e073b9ee6db4fe4eb04f1c7484babbd2c4e06
4
+ data.tar.gz: 9dfb73b083e7e1ac3417a676f37962c1861435310e0f04934d16cea5824f6eb3
5
5
  SHA512:
6
- metadata.gz: a3f7da6a7bca3500190a8edc7087159bdfd3d0ae49008919701849515223145b72364a821bf7e64897485c55da94647d7d74bd394aa5f98d285f16db611ae96c
7
- data.tar.gz: baf53aeee357f38bc4c2d89efde2d3fc4fcbfb2ec7a935ed9968b71f09dc3788e7e6510ae5ef8935e08f3aaff5adea127e34fc8ff2110085667200bb27d71721
6
+ metadata.gz: 4505fedd6b66cf2e5b31fe3cdeb8ec3b337a0c6b8de79082926d637c39dd74770b562280ae6f61bc79f434b2184e993c706dcd7fac088d436b185e56228f7d28
7
+ data.tar.gz: ed2b650393469281ecd997a7589f6e9999a1aeda25e624193c54c61c1d7e7b78b012718945c409c5db8db86588a7ea64e1be547a4d351e92b377329eef1d7e18
data/README.md CHANGED
@@ -6,51 +6,51 @@ 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
- #####note: A reflex will not automatically re-render the component upon its completion. A component will re-render whenever the `set_state` or `refresh!` method is called.
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
- # counter_component.rb
34
- class CounterComponent < ViewComponentReflex::Component
35
-
36
- def initialize
37
- initialize_state({
38
- count: 0
39
- })
40
- end
41
-
42
- reflex :increment do
43
- set_state(count: state[:count] + 1)
44
- end
45
- end
17
+ # counter_component.rb
18
+ class CounterComponent < ViewComponentReflex::Component
19
+ def initialize
20
+ @count = 0
21
+ end
22
+
23
+ def increment
24
+ @count += 1
25
+ end
26
+ end
46
27
  ```
47
28
 
48
29
  ```erb
49
30
  # counter_component.html.erb
50
- <div data-controller="counter">
51
- <p><%= state[:count] %></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
- </div>
34
+ <% end %>
35
+ ```
36
+
37
+ ## Collections
38
+
39
+ In order to reconcile state to components in collections, you can specify a `collection_key` method that returns some
40
+ value unique to that component.
41
+
42
+ ```
43
+ class TodoComponent < ViewComponentReflex::Component
44
+ def initialize(todo:)
45
+ @todo = todo
46
+ end
47
+
48
+ def collection_key
49
+ @todo.id
50
+ end
51
+ end
52
+ #
53
+ <%= render(TodoComponent.with_collection(Todo.all)) %>
54
54
  ```
55
55
 
56
56
  ## Custom State Adapters
@@ -76,36 +76,27 @@ class YourAdapter
76
76
  end
77
77
 
78
78
  ##
79
+ # set_state is used to modify the state. It accepts a reflex, which gives you
80
+ # access to the request, as well as the controller and other useful objects.
81
+ #
79
82
  # reflex - The reflex instance that's trying to set the state
80
83
  # key - a unique string that identifies the component
81
84
  # new_state - the new state to set
82
85
  def self.set_state(reflex, key, new_state)
86
+ # update the state
83
87
  end
84
88
 
85
89
 
86
90
  ##
91
+ # store_state is used to replace the state entirely. It only accepts
92
+ # a request object, rather than a reflex because it's called from the component's
93
+ # side with the component's instance variables.
94
+ #
87
95
  # request - a rails request object
88
96
  # key - a unique string that identifies the component instance
89
97
  # new_state - a hash containing the component state
90
98
  def self.store_state(request, key, new_state = {})
91
- # store the state
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.
99
+ # replace the state
109
100
  end
110
101
  end
111
102
  ```
@@ -1,87 +1,150 @@
1
1
  module ViewComponentReflex
2
2
  class Component < ViewComponent::Base
3
3
  class << self
4
- def reflex(name, &blk)
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 state
12
- ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
13
- end
14
-
15
- def refresh!(primary_selector = "[data-controller=\"#{stimulus_controller}\"]", *selectors)
7
+ def refresh!(primary_selector = "[data-controller~=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]", *selectors)
8
+ save_state
16
9
  @channel.send :render_page_and_broadcast_morph, self, [primary_selector, *selectors], {
17
- dataset: element.dataset.to_h,
18
- args: [],
19
- attrs: element.attributes.to_h,
20
- selectors: ['body'],
21
- target: "#{self.class.name}##{method_name}",
22
- url: request.url,
23
- permanentAttributeName: "data-reflex-permanent"
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"
24
17
  }
25
18
  end
26
19
 
27
20
  def refresh_all!
28
- refresh!(['body'])
21
+ refresh!("body")
29
22
  end
30
23
 
31
- def set_state(new_state = {})
32
- ViewComponentReflex::Engine.state_adapter.set_state(self, element.dataset[:key], new_state)
33
- refresh!
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
34
29
  end
35
30
 
36
- before_reflex do |reflex, *args|
37
- instance_exec(*args, &self.class.callbacks[self.method_name.to_sym]) if self.class.callbacks.include?(self.method_name.to_sym)
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
38
37
  throw :abort
39
38
  end
40
39
 
41
- def self.callbacks
42
- @callbacks ||= {}
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 = [:params, :request, :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
43
72
  end
44
73
 
45
- define_method :stimulus_controller do
46
- klass.name.chomp("Component").underscore.dasherize
74
+ def set_state(new_state = {})
75
+ ViewComponentReflex::Engine.state_adapter.set_state(self, element.dataset[:key], new_state)
76
+ end
77
+
78
+ def state
79
+ ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
47
80
  end
48
81
 
49
- define_singleton_method(:reflex) do |name, &blk|
50
- callbacks[name] = blk
51
- define_method(name) do |*args|
82
+ def save_state
83
+ new_state = {}
84
+ component.instance_variables.each do |k|
85
+ new_state[k] = component.instance_variable_get(k)
52
86
  end
87
+ set_state(new_state)
53
88
  end
54
89
  })
55
90
  end
56
91
  end
57
92
 
58
- def initialize_state(obj)
59
- @state = obj
93
+ def self.stimulus_controller
94
+ name.chomp("Component").underscore.dasherize
60
95
  end
61
96
 
62
97
  def stimulus_reflex?
63
98
  helpers.controller.instance_variable_get(:@stimulus_reflex)
64
99
  end
65
100
 
101
+ def component_controller(opts = {}, &blk)
102
+ self.class.init_stimulus_reflex
103
+ opts[:data] = {
104
+ controller: self.class.stimulus_controller,
105
+ key: key,
106
+ **(opts[:data] || {})
107
+ }
108
+ content_tag :div, capture(&blk), opts
109
+ end
110
+
111
+ def collection_key
112
+ nil
113
+ end
114
+
66
115
  # key is required if you're using state
67
116
  # We can't initialize the session state in the initial method
68
117
  # because it doesn't have a view_context yet
69
118
  # This is the next best place to do it
70
119
  def key
71
- @key ||= caller.find { |p| p.include? ".html.erb" }&.hash.to_s
120
+ # we want the erb file that renders the component. `caller` gives the file name,
121
+ # and line number, which should be unique. We hash it to make it a nice number
122
+ key = caller.select { |p| p.include? ".html.erb" }[1]&.hash.to_s
123
+ key += collection_key.to_s if collection_key
124
+ if @key.nil? || @key.empty?
125
+ @key = key
126
+ end
72
127
 
73
128
  # initialize session state
74
129
  if !stimulus_reflex? || session[@key].nil?
75
- ViewComponentReflex::Engine.state_adapter.store_state(request, @key, @state)
76
- ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", @state)
130
+ new_state = {}
131
+
132
+ # this will almost certainly break
133
+ blacklist = [
134
+ :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
135
+ :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
136
+ :@helpers, :@controller, :@request
137
+ ]
138
+ instance_variables.reject { |k| blacklist.include?(k) }.each do |k|
139
+ new_state[k] = instance_variable_get(k)
140
+ end
141
+ ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
77
142
  else
78
- # ViewComponentReflex::Engine.state_adapter.reconcile_state(request, @key, @state)
143
+ ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v|
144
+ instance_variable_set(k, v)
145
+ end
79
146
  end
80
147
  @key
81
148
  end
82
-
83
- def state
84
- ViewComponentReflex::Engine.state_adapter.state(request, key)
85
- end
86
149
  end
87
150
  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
@@ -1,3 +1,3 @@
1
1
  module ViewComponentReflex
2
- VERSION = '0.6.1'
2
+ VERSION = '1.1.0'
3
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: 0.6.1
4
+ version: 1.1.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 00:00:00.000000000 Z
11
+ date: 2020-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails