view_component_reflex 0.6.3 → 1.2.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: 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