view_component_reflex 0.6.1 → 1.1.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: 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