view_component_reflex 2.2.2 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,148 +1,192 @@
1
- module ViewComponentReflex
2
- class Component < ViewComponent::Base
3
- class << self
4
- def init_stimulus_reflex
5
- @stimulus_reflex ||= if name.include? "::"
6
- module_parent.const_set(name.split("::").last + "Reflex", Class.new(ViewComponentReflex::Reflex))
7
- else
8
- Object.const_set(name + "Reflex", Class.new(ViewComponentReflex::Reflex))
9
- end
10
- @stimulus_reflex.component_class = self
11
- end
12
- end
13
-
14
- def self.stimulus_controller
15
- name.chomp("Component").underscore.dasherize
16
- end
17
-
18
- def stimulus_reflex?
19
- helpers.controller.instance_variable_get(:@stimulus_reflex)
20
- end
21
-
22
- def component_controller(opts_or_tag = :div, opts = {}, &blk)
23
- self.class.init_stimulus_reflex
24
- init_key
25
-
26
- tag = :div
27
- options = if opts_or_tag.is_a? Hash
28
- opts_or_tag
29
- else
30
- tag = opts_or_tag
31
- opts
32
- end
33
- options[:data] = {
34
- controller: self.class.stimulus_controller,
35
- key: key,
36
- **(options[:data] || {})
37
- }
38
- content_tag tag, capture(&blk), options
39
- end
40
-
41
- def can_render_to_string?
42
- omitted_from_state.empty?
43
- end
44
-
45
- # key is required if you're using state
46
- # We can't initialize the session state in the initial method
47
- # because it doesn't have a view_context yet
48
- # This is the next best place to do it
49
- def init_key
50
- # we want the erb file that renders the component. `caller` gives the file name,
51
- # and line number, which should be unique. We hash it to make it a nice number
52
- key = caller.select { |p| p.match? /.\.html\.(haml|erb|slim)/ }[1]&.hash.to_s
53
- key += collection_key.to_s if collection_key
54
- @key = key
55
- end
56
-
57
- # Helper to use to create the proper reflex data attributes for an element
58
- def reflex_data_attributes(reflex)
59
- action, method = reflex.to_s.split("->")
60
- if method.nil?
61
- method = action
62
- action = "click"
63
- end
64
-
65
- {
66
- reflex: "#{action}->#{self.class.name}##{method}",
67
- key: key
68
- }
69
- end
70
-
71
- def reflex_tag(reflex, name, content_or_options_with_block = {}, options = {}, escape = true, &block)
72
- if content_or_options_with_block.is_a?(Hash)
73
- merge_data_attributes(content_or_options_with_block, reflex_data_attributes(reflex))
74
- else
75
- merge_data_attributes(options, reflex_data_attributes(reflex))
76
- end
77
- content_tag(name, content_or_options_with_block, options, escape, &block)
78
- end
79
-
80
- def collection_key
81
- nil
82
- end
83
-
84
- def permit_parameter?(initial_param, new_param)
85
- initial_param != new_param
86
- end
87
-
88
- def omitted_from_state
89
- []
90
- end
91
-
92
- def key
93
- # initialize session state
94
- if !stimulus_reflex? || ViewComponentReflex::Engine.state_adapter.state(request, @key).empty?
95
-
96
- new_state = create_safe_state
97
-
98
- ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
99
- ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state)
100
- else
101
- initial_state = ViewComponentReflex::Engine.state_adapter.state(request, "#{@key}_initial")
102
- ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v|
103
- instance_value = instance_variable_get(k)
104
- if permit_parameter?(initial_state[k], instance_value)
105
- ViewComponentReflex::Engine.state_adapter.set_state(request, controller, "#{@key}_initial", {k => instance_value})
106
- ViewComponentReflex::Engine.state_adapter.set_state(request, controller, @key, {k => instance_value})
107
- else
108
- instance_variable_set(k, v)
109
- end
110
- end
111
- end
112
- @key
113
- end
114
-
115
- def safe_instance_variables
116
- instance_variables - unsafe_instance_variables
117
- end
118
-
119
- private
120
-
121
- def unsafe_instance_variables
122
- [
123
- :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
124
- :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
125
- :@helpers, :@controller, :@request, :@tag_builder
126
- ]
127
- end
128
-
129
- def create_safe_state
130
- new_state = {}
131
-
132
- # this will almost certainly break
133
- safe_instance_variables.each do |k|
134
- new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k)
135
- end
136
- new_state
137
- end
138
-
139
- def merge_data_attributes(options, attributes)
140
- data = options[:data]
141
- if data.nil?
142
- options[:data] = attributes
143
- else
144
- options[:data].merge! attributes
145
- end
146
- end
147
- end
148
- end
1
+ module ViewComponentReflex
2
+ class Component < ViewComponent::Base
3
+ class << self
4
+ def init_stimulus_reflex
5
+ factory = ViewComponentReflex::ReflexFactory.new(self)
6
+ @stimulus_reflex ||= factory.reflex
7
+ wire_up_callbacks if factory.new?
8
+ end
9
+
10
+ def reflex_base_class(new_base_class = nil)
11
+ if new_base_class.nil?
12
+ @reflex_base_class ||= ViewComponentReflex::Reflex
13
+ else
14
+ if new_base_class <= ViewComponentReflex::Reflex
15
+ @reflex_base_class = new_base_class
16
+ else
17
+ raise StandardError.new("The reflex base class must inherit from ViewComponentReflex::Reflex")
18
+ end
19
+ end
20
+ end
21
+
22
+ def queue_callback(key, args, blk)
23
+ callbacks(key).push({
24
+ args: args,
25
+ blk: blk
26
+ })
27
+ end
28
+
29
+ def callbacks(key)
30
+ @callbacks ||= {}
31
+ @callbacks[key] ||= []
32
+ end
33
+
34
+ def register_callbacks(key)
35
+ callbacks(key).each do |cb|
36
+ @stimulus_reflex.send("#{key}_reflex", *cb[:args], &cb[:blk])
37
+ end
38
+ end
39
+
40
+ def before_reflex(*args, &blk)
41
+ queue_callback(:before, args, blk)
42
+ end
43
+
44
+ def after_reflex(*args, &blk)
45
+ queue_callback(:after, args, blk)
46
+ end
47
+
48
+ def around_reflex(*args, &blk)
49
+ queue_callback(:around, args, blk)
50
+ end
51
+
52
+ def wire_up_callbacks
53
+ register_callbacks(:before)
54
+ register_callbacks(:after)
55
+ register_callbacks(:around)
56
+ end
57
+ end
58
+
59
+ def self.stimulus_controller
60
+ name.chomp("Component").underscore.dasherize
61
+ end
62
+
63
+ def stimulus_reflex?
64
+ helpers.controller.instance_variable_get(:@stimulus_reflex)
65
+ end
66
+
67
+ def component_controller(opts_or_tag = :div, opts = {}, &blk)
68
+ init_key
69
+
70
+ tag = :div
71
+ options = if opts_or_tag.is_a? Hash
72
+ opts_or_tag
73
+ else
74
+ tag = opts_or_tag
75
+ opts
76
+ end
77
+ options[:data] = {
78
+ controller: self.class.stimulus_controller,
79
+ key: key,
80
+ **(options[:data] || {})
81
+ }
82
+ content_tag tag, capture(&blk), options
83
+ end
84
+
85
+ def can_render_to_string?
86
+ omitted_from_state.empty?
87
+ end
88
+
89
+ # key is required if you're using state
90
+ # We can't initialize the session state in the initial method
91
+ # because it doesn't have a view_context yet
92
+ # This is the next best place to do it
93
+ def init_key
94
+ # we want the erb file that renders the component. `caller` gives the file name,
95
+ # and line number, which should be unique. We hash it to make it a nice number
96
+ key = caller.select { |p| p.match? /.\.html\.(haml|erb|slim)/ }[1]&.hash.to_s
97
+ key += collection_key.to_s if collection_key
98
+ @key = key
99
+ end
100
+
101
+ # Helper to use to create the proper reflex data attributes for an element
102
+ def reflex_data_attributes(reflex)
103
+ action, method = reflex.to_s.split("->")
104
+ if method.nil?
105
+ method = action
106
+ action = "click"
107
+ end
108
+
109
+ {
110
+ reflex: "#{action}->#{self.class.name}##{method}",
111
+ key: key
112
+ }
113
+ end
114
+
115
+ def reflex_tag(reflex, name, content_or_options_with_block = {}, options = {}, escape = true, &block)
116
+ if content_or_options_with_block.is_a?(Hash)
117
+ merge_data_attributes(content_or_options_with_block, reflex_data_attributes(reflex))
118
+ else
119
+ merge_data_attributes(options, reflex_data_attributes(reflex))
120
+ end
121
+ content_tag(name, content_or_options_with_block, options, escape, &block)
122
+ end
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 omitted_from_state
133
+ []
134
+ end
135
+
136
+ def key
137
+ # initialize session state
138
+ if !stimulus_reflex? || ViewComponentReflex::Engine.state_adapter.state(request, @key).empty?
139
+
140
+ new_state = create_safe_state
141
+
142
+ ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
143
+ ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state)
144
+ else
145
+ initial_state = ViewComponentReflex::Engine.state_adapter.state(request, "#{@key}_initial")
146
+ ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v|
147
+ instance_value = instance_variable_get(k)
148
+ if permit_parameter?(initial_state[k], instance_value)
149
+ ViewComponentReflex::Engine.state_adapter.set_state(request, controller, "#{@key}_initial", {k => instance_value})
150
+ ViewComponentReflex::Engine.state_adapter.set_state(request, controller, @key, {k => instance_value})
151
+ else
152
+ instance_variable_set(k, v)
153
+ end
154
+ end
155
+ end
156
+ @key
157
+ end
158
+
159
+ def safe_instance_variables
160
+ instance_variables - unsafe_instance_variables
161
+ end
162
+
163
+ private
164
+
165
+ def unsafe_instance_variables
166
+ [
167
+ :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
168
+ :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
169
+ :@helpers, :@controller, :@request, :@tag_builder
170
+ ]
171
+ end
172
+
173
+ def create_safe_state
174
+ new_state = {}
175
+
176
+ # this will almost certainly break
177
+ safe_instance_variables.each do |k|
178
+ new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k)
179
+ end
180
+ new_state
181
+ end
182
+
183
+ def merge_data_attributes(options, attributes)
184
+ data = options[:data]
185
+ if data.nil?
186
+ options[:data] = attributes
187
+ else
188
+ options[:data].merge! attributes
189
+ end
190
+ end
191
+ end
192
+ end
@@ -1,9 +1,10 @@
1
- require "stimulus_reflex"
2
- require "view_component_reflex/state_adapter/session"
3
- require "view_component_reflex/state_adapter/memory"
4
- require "view_component_reflex/reflex"
5
- require "view_component_reflex/engine"
6
-
7
- module ViewComponentReflex
8
- # Your code goes here...
9
- end
1
+ require "stimulus_reflex"
2
+ require 'view_component_reflex/reflex_factory'
3
+ require "view_component_reflex/state_adapter/session"
4
+ require "view_component_reflex/state_adapter/memory"
5
+ require "view_component_reflex/reflex"
6
+ require "view_component_reflex/engine"
7
+
8
+ module ViewComponentReflex
9
+ # Your code goes here...
10
+ end
@@ -1,13 +1,36 @@
1
- module ViewComponentReflex
2
- class Engine < ::Rails::Engine
3
- class << self
4
- mattr_accessor :state_adapter
5
-
6
- self.state_adapter = StateAdapter::Memory
7
- end
8
-
9
- def self.configure
10
- yield self if block_given?
11
- end
12
- end
13
- end
1
+ module ViewComponentReflex
2
+ class Engine < ::Rails::Engine
3
+ class << self
4
+ mattr_accessor :state_adapter
5
+
6
+ self.state_adapter = StateAdapter::Memory
7
+ end
8
+
9
+ config.to_prepare do
10
+ StimulusReflex::Channel.class_eval do
11
+ unless instance_methods.include?(:receive_original)
12
+ alias_method :receive_original, :receive
13
+ def receive(data)
14
+ target = data["target"].to_s
15
+ reflex_name, _ = target.split("#")
16
+ reflex_name = reflex_name.camelize
17
+ component_name = reflex_name.end_with?("Reflex") ? reflex_name[0...-6] : reflex_name
18
+ if component_name.end_with?("Component")
19
+ begin
20
+ component_name.constantize.init_stimulus_reflex
21
+ rescue
22
+ p "Tried to initialize view_component_reflex on #{component_name}, but it's not a view_component_reflex"
23
+ end
24
+
25
+ end
26
+ receive_original(data)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.configure
33
+ yield self if block_given?
34
+ end
35
+ end
36
+ end
@@ -1,147 +1,147 @@
1
- module ViewComponentReflex
2
- class Reflex < StimulusReflex::Reflex
3
- include CableReady::Broadcaster
4
-
5
- class << self
6
- attr_accessor :component_class
7
- end
8
-
9
- def refresh!(primary_selector = nil, *rest)
10
- save_state
11
-
12
- if primary_selector.nil? && !component.can_render_to_string?
13
- primary_selector = selector
14
- end
15
- if primary_selector
16
- prevent_refresh!
17
-
18
- controller.process(url_params[:action])
19
- document = Nokogiri::HTML(controller.response.body)
20
- [primary_selector, *rest].each do |s|
21
- html = document.css(s)
22
- if html.present?
23
- cable_ready[channel.stream_name].morph(
24
- selector: s,
25
- html: html.inner_html,
26
- children_only: true,
27
- permanent_attribute_name: "data-reflex-permanent"
28
- )
29
- end
30
- end
31
- else
32
- refresh_component!
33
- end
34
- cable_ready.broadcast
35
- end
36
-
37
- def refresh_component!
38
- component.tap do |k|
39
- k.define_singleton_method(:key) do
40
- element.dataset[:key]
41
- end
42
- end
43
- document = Nokogiri::HTML(controller.render_component_to_string(component))
44
- cable_ready[channel.stream_name].morph(
45
- selector: selector,
46
- children_only: true,
47
- html: document.css(selector).inner_html,
48
- permanent_attribute_name: "data-reflex-permanent"
49
- )
50
- end
51
-
52
- def refresh_all!
53
- refresh!("body")
54
- end
55
-
56
- def selector
57
- "[data-controller~=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]"
58
- end
59
-
60
- # SR's delegate_call_to_reflex in channel.rb
61
- # uses method to gather the method parameters, but since we're abusing
62
- # method_missing here, that'll always fail
63
- def method(name)
64
- component.method(name.to_sym)
65
- end
66
-
67
- def respond_to_missing?(name, _ = false)
68
- !!name.to_proc
69
- end
70
-
71
- # this is copied out of stimulus_reflex/reflex.rb and modified
72
- def morph(selectors, html = "")
73
- case selectors
74
- when :nothing
75
- @broadcaster = StimulusReflex::NothingBroadcaster.new(self)
76
- else
77
- @broadcaster = StimulusReflex::SelectorBroadcaster.new(self) unless broadcaster.selector?
78
- broadcaster.morphs << [selectors, html]
79
- end
80
- end
81
-
82
- def method_missing(name, *args)
83
- morph :nothing
84
- super unless respond_to_missing?(name)
85
- state.each do |k, v|
86
- component.instance_variable_set(k, v)
87
- end
88
- name.to_proc.call(component, *args)
89
- refresh! unless @prevent_refresh
90
- end
91
-
92
- def prevent_refresh!
93
- @prevent_refresh = true
94
- end
95
-
96
- private
97
-
98
- def component_class
99
- self.class.component_class
100
- end
101
-
102
- def stimulus_controller
103
- component_class.stimulus_controller
104
- end
105
-
106
- def stimulate(target, data)
107
- dataset = {}
108
- data.each do |k, v|
109
- dataset["data-#{k}"] = v.to_s
110
- end
111
- channel.receive({
112
- "target" => target,
113
- "attrs" => element.attributes.to_h.symbolize_keys,
114
- "dataset" => dataset
115
- })
116
- end
117
-
118
- def component
119
- return @component if @component
120
- @component = component_class.allocate
121
- reflex = self
122
- exposed_methods = [:params, :request, :element, :refresh!, :refresh_all!, :stimulus_controller, :session, :prevent_refresh!, :selector, :stimulate]
123
- exposed_methods.each do |meth|
124
- @component.define_singleton_method(meth) do |*a|
125
- reflex.send(meth, *a)
126
- end
127
- end
128
- @component
129
- end
130
-
131
- def set_state(new_state = {})
132
- ViewComponentReflex::Engine.state_adapter.set_state(request, controller, element.dataset[:key], new_state)
133
- end
134
-
135
- def state
136
- ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
137
- end
138
-
139
- def save_state
140
- new_state = {}
141
- component.safe_instance_variables.each do |k|
142
- new_state[k] = component.instance_variable_get(k)
143
- end
144
- set_state(new_state)
145
- end
146
- end
147
- end
1
+ module ViewComponentReflex
2
+ class Reflex < StimulusReflex::Reflex
3
+ include CableReady::Broadcaster
4
+
5
+ class << self
6
+ attr_accessor :component_class
7
+ end
8
+
9
+ def refresh!(primary_selector = nil, *rest)
10
+ save_state
11
+
12
+ if primary_selector.nil? && !component.can_render_to_string?
13
+ primary_selector = selector
14
+ end
15
+ if primary_selector
16
+ prevent_refresh!
17
+
18
+ controller.process(url_params[:action])
19
+ document = Nokogiri::HTML(controller.response.body)
20
+ [primary_selector, *rest].each do |s|
21
+ html = document.css(s)
22
+ if html.present?
23
+ cable_ready[channel.stream_name].morph(
24
+ selector: s,
25
+ html: html.inner_html,
26
+ children_only: true,
27
+ permanent_attribute_name: "data-reflex-permanent"
28
+ )
29
+ end
30
+ end
31
+ else
32
+ refresh_component!
33
+ end
34
+ cable_ready.broadcast
35
+ end
36
+
37
+ def refresh_component!
38
+ component.tap do |k|
39
+ k.define_singleton_method(:key) do
40
+ element.dataset[:key]
41
+ end
42
+ end
43
+ document = Nokogiri::HTML(controller.render_component_to_string(component))
44
+ cable_ready[channel.stream_name].morph(
45
+ selector: selector,
46
+ children_only: true,
47
+ html: document.css(selector).inner_html,
48
+ permanent_attribute_name: "data-reflex-permanent"
49
+ )
50
+ end
51
+
52
+ def refresh_all!
53
+ refresh!("body")
54
+ end
55
+
56
+ def selector
57
+ "[data-controller~=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]"
58
+ end
59
+
60
+ # SR's delegate_call_to_reflex in channel.rb
61
+ # uses method to gather the method parameters, but since we're abusing
62
+ # method_missing here, that'll always fail
63
+ def method(name)
64
+ component.method(name.to_sym)
65
+ end
66
+
67
+ def respond_to_missing?(name, _ = false)
68
+ !!name.to_proc
69
+ end
70
+
71
+ # this is copied out of stimulus_reflex/reflex.rb and modified
72
+ def morph(selectors, html = "")
73
+ case selectors
74
+ when :nothing
75
+ @broadcaster = StimulusReflex::NothingBroadcaster.new(self)
76
+ else
77
+ @broadcaster = StimulusReflex::SelectorBroadcaster.new(self) unless broadcaster.selector?
78
+ broadcaster.morphs << [selectors, html]
79
+ end
80
+ end
81
+
82
+ def method_missing(name, *args)
83
+ morph :nothing
84
+ super unless respond_to_missing?(name)
85
+ state.each do |k, v|
86
+ component.instance_variable_set(k, v)
87
+ end
88
+ name.to_proc.call(component, *args)
89
+ refresh! unless @prevent_refresh
90
+ end
91
+
92
+ def prevent_refresh!
93
+ @prevent_refresh = true
94
+ end
95
+
96
+ private
97
+
98
+ def component_class
99
+ self.class.component_class
100
+ end
101
+
102
+ def stimulus_controller
103
+ component_class.stimulus_controller
104
+ end
105
+
106
+ def stimulate(target, data)
107
+ dataset = {}
108
+ data.each do |k, v|
109
+ dataset["data-#{k}"] = v.to_s
110
+ end
111
+ channel.receive({
112
+ "target" => target,
113
+ "attrs" => element.attributes.to_h.symbolize_keys,
114
+ "dataset" => dataset
115
+ })
116
+ end
117
+
118
+ def component
119
+ return @component if @component
120
+ @component = component_class.allocate
121
+ reflex = self
122
+ exposed_methods = [:params, :request, :element, :refresh!, :refresh_all!, :stimulus_controller, :session, :prevent_refresh!, :selector, :stimulate]
123
+ exposed_methods.each do |meth|
124
+ @component.define_singleton_method(meth) do |*a|
125
+ reflex.send(meth, *a)
126
+ end
127
+ end
128
+ @component
129
+ end
130
+
131
+ def set_state(new_state = {})
132
+ ViewComponentReflex::Engine.state_adapter.set_state(request, controller, element.dataset[:key], new_state)
133
+ end
134
+
135
+ def state
136
+ ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
137
+ end
138
+
139
+ def save_state
140
+ new_state = {}
141
+ component.safe_instance_variables.each do |k|
142
+ new_state[k] = component.instance_variable_get(k)
143
+ end
144
+ set_state(new_state)
145
+ end
146
+ end
147
+ end