view_component_reflex 0.6.3 → 1.2.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: 25f3eb9ce0b0a130b3448c9aa350636e86f4497c9274a1a4ba7edfb564d11a16
4
- data.tar.gz: 809b06a816a92e8ba1e3ce0198c55c6f3d812a0ad3c4b447300c56e4733102b4
3
+ metadata.gz: 4ed2f845769bba6d5142bef6d6fc601b44841080c49ee47abc0036801f62b145
4
+ data.tar.gz: 73e625d1948e5b477def8e098a4a4d55b9a02763c575e1d4bdd4c734e0f208b0
5
5
  SHA512:
6
- metadata.gz: e3c0cf57dda891149ccd671d02fbc967aaa83277f789a1cd0c7c75192fa4a83e3e593177ef49f904cbc9cc8bb241b9506527bfafee0b90b049252b1822a4cb68
7
- data.tar.gz: 94c5b424ae03b1f35f797ddebf0d6f24bf78b05960140c415cce86d4c2bf645ce4eef7445d7049ef57ab0d2f26925bac251b0a202199840f23f4f0adee22fc17
6
+ metadata.gz: 76c5b53b3acb0fa68084b762ad14fddbaa0aee1d0b9bbe67f33910f088e7b60062bf5dcf61c7703932b2cfa228f4bccfc768e6b9ab64253397e3483efd50cfb5
7
+ data.tar.gz: 077a34955920288651b183d77971d30eb4fbfeabe45aaa799d78f2ea242cb46f328d23223508d053e53210c00e9aeb570e9d234b571aba3005e57b6448afd5fc
data/README.md CHANGED
@@ -6,53 +6,60 @@ 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 %>
54
35
  ```
55
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
+ ```
55
+
56
+ ## API
57
+
58
+ ### permit_parameter?(initial_param, new_params)
59
+ If a new parameter is passed to the component during rendering, it is used instead of what's in state.
60
+ If you're storing instances in state, you can use this to properly compare them.
61
+
62
+
56
63
  ## Custom State Adapters
57
64
 
58
65
  ViewComponentReflex uses session for its state by default. To change this, add
@@ -76,19 +83,27 @@ class YourAdapter
76
83
  end
77
84
 
78
85
  ##
79
- # reflex - The reflex instance that's trying to set the state
86
+ # set_state is used to modify the state.
87
+ #
88
+ # request - a rails request object
89
+ # controller - the current controller
80
90
  # key - a unique string that identifies the component
81
91
  # new_state - the new state to set
82
- def self.set_state(reflex, key, new_state)
92
+ def self.set_state(request, controller, key, new_state)
93
+ # update the state
83
94
  end
84
95
 
85
96
 
86
97
  ##
98
+ # store_state is used to replace the state entirely. It only accepts
99
+ # a request object, rather than a reflex because it's called from the component's
100
+ # side with the component's instance variables.
101
+ #
87
102
  # request - a rails request object
88
103
  # key - a unique string that identifies the component instance
89
104
  # new_state - a hash containing the component state
90
105
  def self.store_state(request, key, new_state = {})
91
- # store the state
106
+ # replace the state
92
107
  end
93
108
  end
94
109
  ```
@@ -1,84 +1,159 @@
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")
22
+ end
23
+
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
29
  end
30
30
 
31
- def set_state(new_state = {}, primary_selector = nil, *selectors)
32
- ViewComponentReflex::Engine.state_adapter.set_state(self, element.dataset[:key], new_state)
33
- refresh!(primary_selector, *selectors)
31
+ def respond_to_missing?(name, _ = false)
32
+ !!name.to_proc
34
33
  end
35
34
 
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)
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
43
51
  end
44
52
 
45
- define_method :stimulus_controller do
46
- klass.name.chomp("Component").underscore.dasherize
53
+ private :component_class
54
+
55
+ private
56
+
57
+ def stimulus_controller
58
+ component_class.stimulus_controller
47
59
  end
48
60
 
49
- define_singleton_method(:reflex) do |name, &blk|
50
- callbacks[name] = blk
51
- define_method(name) do |*args|
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
52
70
  end
71
+ @component
72
+ end
73
+
74
+ def set_state(new_state = {})
75
+ ViewComponentReflex::Engine.state_adapter.set_state(request, controller, element.dataset[:key], new_state)
76
+ end
77
+
78
+ def state
79
+ ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
80
+ end
81
+
82
+ def save_state
83
+ new_state = {}
84
+ component.instance_variables.each do |k|
85
+ new_state[k] = component.instance_variable_get(k)
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
+ init_key
104
+ opts[:data] = {
105
+ controller: self.class.stimulus_controller,
106
+ key: key,
107
+ **(opts[:data] || {})
108
+ }
109
+ content_tag :div, capture(&blk), opts
110
+ end
111
+
66
112
  # key is required if you're using state
67
113
  # We can't initialize the session state in the initial method
68
114
  # because it doesn't have a view_context yet
69
115
  # This is the next best place to do it
70
- def key
71
- @key ||= caller.find { |p| p.include? ".html.erb" }&.hash.to_s
116
+ def init_key
117
+ # we want the erb file that renders the component. `caller` gives the file name,
118
+ # and line number, which should be unique. We hash it to make it a nice number
119
+ key = caller.select { |p| p.include? ".html.erb" }[1]&.hash.to_s
120
+ key += collection_key.to_s if collection_key
121
+ @key = key
122
+ end
72
123
 
124
+ def collection_key
125
+ nil
126
+ end
127
+
128
+ def permit_parameter?(initial_param, new_param)
129
+ initial_param != new_param
130
+ end
131
+
132
+ def key
73
133
  # initialize session state
74
134
  if !stimulus_reflex? || session[@key].nil?
75
- ViewComponentReflex::Engine.state_adapter.store_state(request, @key, @state)
135
+ new_state = {}
136
+
137
+ # this will almost certainly break
138
+ blacklist = [
139
+ :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
140
+ :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
141
+ :@helpers, :@controller, :@request, :@content
142
+ ]
143
+ instance_variables.reject { |k| blacklist.include?(k) }.each do |k|
144
+ new_state[k] = instance_variable_get(k)
145
+ end
146
+ ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
147
+ ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state)
148
+ else
149
+ initial_state = ViewComponentReflex::Engine.state_adapter.state(request, "#{@key}_initial")
150
+ ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v|
151
+ unless permit_parameter?(initial_state[k], instance_variable_get(k))
152
+ instance_variable_set(k, v)
153
+ end
154
+ end
76
155
  end
77
156
  @key
78
157
  end
79
-
80
- def state
81
- ViewComponentReflex::Engine.state_adapter.state(request, key)
82
- end
83
158
  end
84
159
  end
@@ -5,12 +5,12 @@ module ViewComponentReflex
5
5
  request.session[key] ||= {}
6
6
  end
7
7
 
8
- def self.set_state(reflex, key, new_state)
8
+ def self.set_state(request, controller, key, new_state)
9
9
  new_state.each do |k, v|
10
- state(reflex.request, key)[k] = v
10
+ state(request, key)[k] = v
11
11
  end
12
- store = reflex.request.session.instance_variable_get("@by")
13
- store.commit_session reflex.request, reflex.controller.response
12
+ store = request.session.instance_variable_get("@by")
13
+ store.commit_session request, controller.response
14
14
  end
15
15
 
16
16
  def self.store_state(request, key, new_state = {})
@@ -1,3 +1,3 @@
1
1
  module ViewComponentReflex
2
- VERSION = '0.6.3'
2
+ VERSION = '1.2.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.3
4
+ version: 1.2.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-15 00:00:00.000000000 Z
11
+ date: 2020-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails