view_component_reflex 3.1.14.pre9 → 3.2.0.pre

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: 92c0d1350e67ae8b1ae796fc57f4ce22f7cddebb0bce2559ba25df5e81dc191a
4
- data.tar.gz: 39c96a9635ce1d217a6a1bf772743c7e9103ebf30b748cf22756614ea13030d3
3
+ metadata.gz: '076282b09956e12df3434c1be972995da10bdc2a094598f71da922bef7f84f6e'
4
+ data.tar.gz: c250e5e4de40beafa81110a32e9d4ef03b0321d3a1674f2ba4811b56bdffe68a
5
5
  SHA512:
6
- metadata.gz: ceb51a750f14878ecab26760e2c3951259ff9f3c7da3b503f504b7197d7d2163d5faf95b063b55a77bdfc662467422b22a987f3b7adb4f5ae364cfdeb4aee16c
7
- data.tar.gz: c8e96efd021ad1c46deee61537e60a364dc6b5636a54985389deb5de3094774f7b3d941de07cf2a17616aec16b96d161d9ee2f2c619c8dd1fec95f1e2fa180f4
6
+ metadata.gz: 9dd881a3cb9a890f771a5ed21df13dc15eb73db2ed6beb290f43d9fa3967d58a0631833a653ebf0e49eb2e44e3a5651e0855174df42e7bdf093e876959af75fd
7
+ data.tar.gz: 2f801a13e7e1b081b205b7def36cf0f7ef42b44fe4f81f0c55781202252c2eb8548ab4d84895577eab727262e70c0e4377dc54584b9cff656d01c9dfb41dabaa
data/README.md CHANGED
@@ -248,6 +248,27 @@ class MyComponent < ViewComponentReflex::Component
248
248
  end
249
249
  ```
250
250
 
251
+ ## Per-component state adapters
252
+ You can override the default state adapter for a component by using the `state_adapter` helper.
253
+
254
+ ```ruby
255
+ class MyComponent < ViewComponentReflex::Component
256
+ state_adapter :dom # or :session, or :memory
257
+ end
258
+ ```
259
+
260
+ This will also accept a fully qualified constant
261
+
262
+ ```ruby
263
+ class MyComponent < ViewComponentReflex::Component
264
+ state_adapter ViewComponentReflex::StateAdapter::Redis.new(
265
+ redis_opts: {
266
+ url: "redis://localhost:6379/1", driver: :hiredis
267
+ },
268
+ ttl: 3600)
269
+ end
270
+ ```
271
+
251
272
  ## Common patterns
252
273
  A lot of the time, you only need to update specific components when changing instance variables. For example, changing `@loading` might only need
253
274
  to display a spinner somewhere on the page. You can define setters to implicitly render the appropriate pieces of dom whenever that variable is set
@@ -288,6 +309,9 @@ end
288
309
  By default (since version `2.3.2`), view_component_reflex stores component state in session. You can optionally set the state adapter
289
310
  to use the memory by changing `config.state_adapter` to `ViewComponentReflex::StateAdapter::Memory`.
290
311
 
312
+ Optionally, you can also store state right in the dom with `ViewComponentReflex::StateAdapter::Dom`. Not that the DOM
313
+ adapter requires the `data-reflex-dataset="*"` property to be set on anything firing the reflex.
314
+
291
315
  ## Custom State Adapters
292
316
 
293
317
  ViewComponentReflex uses session for its state by default. To change this, add
@@ -299,7 +323,6 @@ ViewComponentReflex::Engine.configure do |config|
299
323
  end
300
324
  ```
301
325
 
302
-
303
326
  ## Existing Fast Redis based State Adapter
304
327
 
305
328
  This adapter uses hmset and hgetall to reduce the number of operations.
data/Rakefile CHANGED
@@ -14,8 +14,8 @@ RDoc::Task.new(:rdoc) do |rdoc|
14
14
  rdoc.rdoc_files.include('lib/**/*.rb')
15
15
  end
16
16
 
17
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
- load 'rails/tasks/engine.rake'
17
+ # APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
+ # load 'rails/tasks/engine.rake'
19
19
 
20
20
  load 'rails/tasks/statistics.rake'
21
21
 
@@ -4,6 +4,8 @@ module ViewComponentReflex
4
4
  attr_reader :key
5
5
 
6
6
  class << self
7
+ attr_reader :_state_adapter
8
+
7
9
  def init_stimulus_reflex
8
10
  factory = ViewComponentReflex::ReflexFactory.new(self)
9
11
  @stimulus_reflex ||= factory.reflex
@@ -57,6 +59,15 @@ module ViewComponentReflex
57
59
  register_callbacks(:after)
58
60
  register_callbacks(:around)
59
61
  end
62
+
63
+ def state_adapter(what)
64
+ if what.is_a?(Symbol) || what.is_a?(String)
65
+ class_name = what.to_s.camelize
66
+ @_state_adapter = StateAdapter.const_get class_name
67
+ else
68
+ @_state_adapter = what
69
+ end
70
+ end
60
71
  end
61
72
 
62
73
  def self.stimulus_controller
@@ -67,6 +78,10 @@ module ViewComponentReflex
67
78
  helpers.controller.instance_variable_get(:@stimulus_reflex)
68
79
  end
69
80
 
81
+ def before_render
82
+ adapter.extend_component(self)
83
+ end
84
+
70
85
  def component_controller(opts_or_tag = :div, opts = {}, &blk)
71
86
  initialize_component
72
87
 
@@ -91,10 +106,10 @@ module ViewComponentReflex
91
106
  end
92
107
 
93
108
  # We can't truly initialize the component without the view_context,
94
- # which isn't available in the `initialize` method. We require the
109
+ # which isn't available in the `initialize` method. We require the
95
110
  # developer to wrap components in `component_controller`, so this is where
96
111
  # we truly initialize the component.
97
- # This method is overridden in reflex.rb when the component is re-rendered. The
112
+ # This method is overridden in reflex.rb when the component is re-rendered. The
98
113
  # override simply sets @key to element.dataset[:key]
99
114
  # We don't want it to initialize the state again, and since we're rendering the component
100
115
  # outside of the view, we need to skip the initialize_key method as well
@@ -109,30 +124,31 @@ module ViewComponentReflex
109
124
  # so we can't rely on the component created by the reflex
110
125
  def initialize_state
111
126
  return if state_initialized?
112
- adapter = ViewComponentReflex::Engine.state_adapter
113
-
127
+
128
+ adapter.extend_component(self)
129
+
114
130
  # newly mounted
115
- if !stimulus_reflex? || adapter.state(request, @key).empty?
131
+ if !stimulus_reflex? || state(@key).empty?
116
132
 
117
133
  new_state = create_safe_state
118
134
 
119
- adapter.wrap_write_async do
120
- adapter.store_state(request, @key, new_state)
121
- adapter.store_state(request, "#{@key}_initial", new_state)
135
+ wrap_write_async do
136
+ store_state(@key, new_state)
137
+ store_state("#{@key}_initial", new_state)
122
138
  end
123
139
 
124
140
  # updating a mounted component
125
141
  else
126
- initial_state = adapter.state(request, "#{@key}_initial")
142
+ initial_state = state("#{@key}_initial")
127
143
 
128
144
  parameters_changed = []
129
- adapter.state(request, @key).each do |k, v|
145
+ state(@key).each do |k, v|
130
146
  instance_value = instance_variable_get(k)
131
147
  if permit_parameter?(initial_state[k], instance_value)
132
148
  parameters_changed << k
133
- adapter.wrap_write_async do
134
- adapter.set_state(request, controller, "#{@key}_initial", {k => instance_value})
135
- adapter.set_state(request, controller, @key, {k => instance_value})
149
+ wrap_write_async do
150
+ set_state("#{@key}_initial", { k => instance_value })
151
+ set_state(@key, { k => instance_value })
136
152
  end
137
153
  else
138
154
  instance_variable_set(k, v)
@@ -143,6 +159,26 @@ module ViewComponentReflex
143
159
  @state_initialized = true
144
160
  end
145
161
 
162
+ def adapter
163
+ self.class._state_adapter || ViewComponentReflex::Engine.state_adapter
164
+ end
165
+
166
+ def wrap_write_async(&blk)
167
+ adapter.wrap_write_async(&blk)
168
+ end
169
+
170
+ def set_state(key, new_state)
171
+ adapter.set_state(request, controller, key, new_state)
172
+ end
173
+
174
+ def state(key)
175
+ adapter.state(request, key)
176
+ end
177
+
178
+ def store_state(key, new_state = {})
179
+ adapter.store_state(request, key, new_state)
180
+ end
181
+
146
182
  def state_initialized?
147
183
  @state_initialized
148
184
  end
@@ -152,10 +188,10 @@ module ViewComponentReflex
152
188
  # and line number, which should be unique. We hash it to make it a nice number
153
189
  erb_file = caller.select { |p| p.match? /.\.html\.(haml|erb|slim)/ }[1]
154
190
  key = if erb_file
155
- Digest::SHA2.hexdigest(erb_file.split(":in")[0])
156
- else
157
- ""
158
- end
191
+ Digest::SHA2.hexdigest(erb_file.split(":in")[0])
192
+ else
193
+ ""
194
+ end
159
195
  key += collection_key.to_s if collection_key
160
196
  @key = key
161
197
  end
@@ -0,0 +1,40 @@
1
+ module ViewComponentReflex
2
+ module Dom
3
+ module Component
4
+
5
+ def component_controller(opts_or_tag = :div, opts = {}, &blk)
6
+ initialize_component
7
+
8
+ tag = :div
9
+ options = if opts_or_tag.is_a? Hash
10
+ opts_or_tag
11
+ else
12
+ tag = opts_or_tag
13
+ opts
14
+ end
15
+
16
+ data = {
17
+ "#{key}_state" => Verifier.generate(state(key)),
18
+ "#{key}_initial" => Verifier.generate(state("#{key}_initial")),
19
+ }
20
+
21
+ options[:data] = {
22
+ controller: self.class.stimulus_controller,
23
+ key: key,
24
+ **(options[:data] || {})
25
+ }
26
+
27
+ content_tag tag, options do
28
+ concat(content_tag(:span, nil, { data: data, style: "display: none;" }))
29
+ concat(capture(&blk))
30
+ end
31
+ end
32
+
33
+ def reflex_data_attributes(reflex)
34
+ super(reflex).tap do |attr|
35
+ attr["reflex-dataset"] = "*"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ ##
2
+ # State adapters assume that the data lives outside of the rendering pipeline
3
+ # By moving the state to the DOM, we need to "hydrate" the state when the reflex comes in with the data
4
+ # We're overriding `inject_key_into_component` for this purpose, since it's run once during a reflex,
5
+ # and _just_ after the component is initialized, but before it actually does anything.
6
+ # This ensures that any actions that the component is taking is going to be operating on the correct
7
+ # state
8
+
9
+ module ViewComponentReflex
10
+ module Dom
11
+ module Reflex
12
+ def state
13
+ if element.dataset[:"#{key}_state"]
14
+ Verifier.verify(element.dataset[:"#{key}_state"])
15
+ else
16
+ {}
17
+ end
18
+ end
19
+
20
+ def inject_key_into_component
21
+ super
22
+
23
+ p initial_state[:@count]
24
+ state_adapter.store_state(request, key, state)
25
+ state_adapter.store_state(request, "#{key}_initial", initial_state)
26
+ end
27
+
28
+ def initial_state
29
+ if element.dataset[:"#{key}_initial"]
30
+ Verifier.verify(element.dataset[:"#{key}_initial"])
31
+ else
32
+ {}
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ module ViewComponentReflex
2
+ module Dom
3
+ class Verifier
4
+ class << self
5
+ attr_reader :verifier
6
+
7
+ delegate :generate, :verified, :verify, :valid_message?, to: :verifier
8
+
9
+ def verifier
10
+ Rails.application.message_verifier(:view_component_reflex)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,6 +1,5 @@
1
1
  module ViewComponentReflex
2
2
  class Reflex < StimulusReflex::Reflex
3
-
4
3
  class << self
5
4
  attr_accessor :component_class
6
5
  end
@@ -48,21 +47,23 @@ module ViewComponentReflex
48
47
  @stream = channel
49
48
  end
50
49
 
51
- def component_document
50
+ def inject_key_into_component
52
51
  component.tap do |k|
53
52
  k.define_singleton_method(:initialize_component) do
54
53
  @key = element.dataset[:key]
55
54
  end
56
55
  end
56
+ end
57
57
 
58
- document = Nokogiri::HTML(component.render_in(controller.view_context))
58
+ def component_document
59
+ Nokogiri::HTML(component.render_in(controller.view_context))
59
60
  end
60
61
 
61
62
  def refresh_component!
62
63
  CableReady::Channels.instance[stream].morph(
63
64
  selector: selector,
64
65
  children_only: true,
65
- html: component_document.css(selector).inner_html,
66
+ html: component_document.css(selector).to_html,
66
67
  permanent_attribute_name: "data-reflex-permanent",
67
68
  reflex_id: reflex_id
68
69
  )
@@ -109,6 +110,7 @@ module ViewComponentReflex
109
110
  # uses method to gather the method parameters, but since we're abusing
110
111
  # method_missing here, that'll always fail
111
112
  def method(name)
113
+ component.adapter.extend_reflex(self)
112
114
  component.method(name.to_sym)
113
115
  end
114
116
 
@@ -124,11 +126,11 @@ module ViewComponentReflex
124
126
  end
125
127
 
126
128
  component.send(name, *args, &blk)
127
-
129
+
128
130
  if @prevent_refresh
129
131
  morph :nothing
130
132
  else
131
- default_morph
133
+ default_morph
132
134
  end
133
135
  end
134
136
 
@@ -196,6 +198,8 @@ module ViewComponentReflex
196
198
  reflex
197
199
  end
198
200
 
201
+ inject_key_into_component
202
+
199
203
  @component
200
204
  end
201
205
 
@@ -208,7 +212,7 @@ module ViewComponentReflex
208
212
  end
209
213
 
210
214
  def state_adapter
211
- ViewComponentReflex::Engine.state_adapter
215
+ component.adapter
212
216
  end
213
217
 
214
218
  def state
@@ -2,6 +2,30 @@ module ViewComponentReflex
2
2
  module StateAdapter
3
3
  class Base
4
4
 
5
+ def self.state(request, key)
6
+ # stub
7
+ end
8
+
9
+ def self.set_state(request, controller, key, new_state)
10
+ # stub
11
+ end
12
+
13
+ def self.store_state(request, key, new_state = {})
14
+ # stub
15
+ end
16
+
17
+ def self.wrap_write_async
18
+ yield
19
+ end
20
+
21
+ def self.extend_component(component)
22
+ # stub
23
+ end
24
+
25
+ def self.extend_reflex(component)
26
+ # stub
27
+ end
28
+
5
29
  private
6
30
 
7
31
  def self.extract_id(request)
@@ -0,0 +1,56 @@
1
+ ##
2
+ # This adapter exists solely to allow switching to dom-based state storage, even
3
+ # though dom-based state storage doesn't abide by the same standards as the other adapters
4
+ #
5
+ # The dom-based state storage is handled by the Dom::Component and Dom::Reflex modules that are prepended
6
+ # to Component and Reflex in Engine, if the dom adapter is selected
7
+ #
8
+ # These modules override various methods to inject state into the dom, as well as encoding/decoding the state from the dom
9
+
10
+ class CurrentState < ActiveSupport::CurrentAttributes
11
+ attribute :state
12
+ end
13
+
14
+ module ViewComponentReflex
15
+ module StateAdapter
16
+ class Dom < Base
17
+ def self.state(request, key)
18
+ id = extract_id(request)
19
+
20
+ CurrentState.state ||= {}
21
+ CurrentState.state[id] ||= {}
22
+ CurrentState.state[id][key] ||= {}
23
+ end
24
+
25
+ def self.set_state(request, _, key, new_state)
26
+ new_state.each do |k, v|
27
+ state(request, key)[k] = v
28
+ end
29
+ end
30
+
31
+ def self.store_state(request, key, new_state = {})
32
+ id = extract_id(request)
33
+
34
+ CurrentState.state ||= {}
35
+ CurrentState.state[id] ||= {}
36
+ CurrentState.state[id][key] = {}
37
+
38
+ new_state.each do |k, v|
39
+ CurrentState.state[id][key][k] = v
40
+ end
41
+ end
42
+
43
+ def self.extend_component(component)
44
+ component.extend ViewComponentReflex::Dom::Component
45
+ end
46
+
47
+ def self.extend_reflex(reflex)
48
+ reflex.extend ViewComponentReflex::Dom::Reflex
49
+ end
50
+
51
+ def self.wrap_write_async
52
+ yield
53
+ end
54
+ end
55
+ end
56
+ end
@@ -11,7 +11,8 @@
11
11
  module ViewComponentReflex
12
12
  module StateAdapter
13
13
  class Redis < Base
14
- attr_reader :client, :ttl
14
+ attr_reader :ttl
15
+ attr_accessor :client
15
16
 
16
17
  def initialize(redis_opts:, ttl: 3600)
17
18
  @client = ::Redis.new(redis_opts)
@@ -40,11 +41,22 @@ module ViewComponentReflex
40
41
  end
41
42
 
42
43
  def wrap_write_async
43
- client.pipelined do
44
+ client.pipelined do |pipeline|
45
+ original_client = client
46
+ @client = pipeline
44
47
  yield
48
+ @client = original_client
45
49
  end
46
50
  end
47
51
 
52
+ def extend_component(_)
53
+ #stub
54
+ end
55
+
56
+ def extend_reflex(_)
57
+ #stub
58
+ end
59
+
48
60
  private
49
61
 
50
62
  # Reduce number of calls coming from #store_state to save Redis
@@ -1,3 +1,3 @@
1
1
  module ViewComponentReflex
2
- VERSION = '3.1.14.pre9'
2
+ VERSION = '3.2.0.pre'
3
3
  end
@@ -3,9 +3,14 @@ require 'view_component_reflex/reflex_factory'
3
3
  require "view_component_reflex/state_adapter/base"
4
4
  require "view_component_reflex/state_adapter/session"
5
5
  require "view_component_reflex/state_adapter/memory"
6
+ require "view_component_reflex/state_adapter/dom"
6
7
  require "view_component_reflex/state_adapter/redis"
7
- require "view_component_reflex/reflex"
8
+ require "view_component_reflex/dom/verifier"
9
+ require "view_component_reflex/dom/reflex"
10
+ require "view_component_reflex/dom/component"
8
11
  require "view_component_reflex/engine"
12
+ require "view_component_reflex/reflex"
13
+ require "view_component_reflex/component"
9
14
 
10
15
  module ViewComponentReflex
11
16
  # Your code goes here...
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: 3.1.14.pre9
4
+ version: 3.2.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua LeBlanc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-10 00:00:00.000000000 Z
11
+ date: 2022-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -82,12 +82,16 @@ files:
82
82
  - MIT-LICENSE
83
83
  - README.md
84
84
  - Rakefile
85
- - app/components/view_component_reflex/component.rb
86
85
  - lib/view_component_reflex.rb
86
+ - lib/view_component_reflex/component.rb
87
+ - lib/view_component_reflex/dom/component.rb
88
+ - lib/view_component_reflex/dom/reflex.rb
89
+ - lib/view_component_reflex/dom/verifier.rb
87
90
  - lib/view_component_reflex/engine.rb
88
91
  - lib/view_component_reflex/reflex.rb
89
92
  - lib/view_component_reflex/reflex_factory.rb
90
93
  - lib/view_component_reflex/state_adapter/base.rb
94
+ - lib/view_component_reflex/state_adapter/dom.rb
91
95
  - lib/view_component_reflex/state_adapter/memory.rb
92
96
  - lib/view_component_reflex/state_adapter/redis.rb
93
97
  - lib/view_component_reflex/state_adapter/session.rb
@@ -112,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
116
  - !ruby/object:Gem::Version
113
117
  version: 1.3.1
114
118
  requirements: []
115
- rubygems_version: 3.2.9
119
+ rubygems_version: 3.3.3
116
120
  signing_key:
117
121
  specification_version: 4
118
122
  summary: Allow stimulus reflexes in a view component