view_component_reflex 0.5.1 → 1.0.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: 730dc20cb5c31f500fb8c6fd63c5e21286a4a3ea28f63a64aaa06c1640ef4844
4
- data.tar.gz: 911b8665fd95a69b28de2bf366ffa9edb4240070c8e29c145090d27ba8eaf271
3
+ metadata.gz: f7e9a4ab35ecb85fef158f336121ffd080f26d344bbf804abfc1fd85849b6734
4
+ data.tar.gz: 202ab52b2ad27cca0b823cb13d71b0389d6dba68e35e1f92da2ef601a2690125
5
5
  SHA512:
6
- metadata.gz: 679abeb33702936136cfc32244be5bd7ed85cbe8526594eb3f3ed8138b0aec7e004507eab5c4624fa0accd19958999a62641122dbfd67693ab9c3bf11424fd9f
7
- data.tar.gz: 97f3b2645a43893d09a253c3d8c780ce1c141aab599c67db3fb6bd47ed443780960d831da6b10ff2930a1bdb0eb063ff667c5292b1c23f1e6eb9f1292f890c40
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
- #####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
17
  # counter_component.rb
34
18
  class CounterComponent < ViewComponentReflex::Component
35
-
36
19
  def initialize
37
- initialize_state({
38
- count: 0
39
- })
20
+ @count = 0
40
21
  end
41
22
 
42
- reflex :increment do
43
- set_state(count: state[:count] + 1)
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
- <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 %>
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
- # 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.
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 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!
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: ['body'],
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 set_state(new_state = {})
28
- ViewComponentReflex::Engine.state_adapter.set_state(self, element.dataset[:key], new_state)
29
- refresh!
20
+ def refresh_all!
21
+ refresh!("body")
30
22
  end
31
23
 
32
- before_reflex do |reflex, *args|
33
- instance_exec(*args, &self.class.callbacks[self.method_name.to_sym]) if self.class.callbacks.include?(self.method_name.to_sym)
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 self.callbacks
38
- @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 = [: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
- define_method :stimulus_controller do
42
- klass.name.chomp("Component").underscore.dasherize
78
+ def state
79
+ ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
43
80
  end
44
81
 
45
- define_singleton_method(:reflex) do |name, &blk|
46
- callbacks[name] = blk
47
- 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)
48
86
  end
87
+ set_state(new_state)
49
88
  end
50
89
  })
51
90
  end
52
91
  end
53
92
 
54
- def initialize_state(obj)
55
- @state = obj
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
- @key ||= caller.find { |p| p.include? ".html.erb" }&.hash.to_s
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
- ViewComponentReflex::Engine.state_adapter.store_state(request, @key, @state)
72
- ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", @state)
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
- # ViewComponentReflex::Engine.state_adapter.reconcile_state(request, @key, @state)
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
@@ -1,3 +1,3 @@
1
1
  module ViewComponentReflex
2
- VERSION = '0.5.1'
2
+ VERSION = '1.0.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.5.1
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 00:00:00.000000000 Z
11
+ date: 2020-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails