live_cable 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +567 -0
- data/README.md +14 -2
- data/app/assets/javascript/controllers/live_controller.js +51 -7
- data/app/assets/javascript/live_cable.js +2 -0
- data/app/assets/javascript/loading.js +58 -6
- data/app/assets/javascript/subscriptions.js +292 -22
- data/app/channels/live_channel.rb +51 -29
- data/app/helpers/live_cable_helper.rb +31 -2
- data/lib/live_cable/action_cable_connection.rb +4 -1
- data/lib/live_cable/component/broadcasting.rb +37 -6
- data/lib/live_cable/component/dispatching.rb +36 -0
- data/lib/live_cable/component/lifecycle.rb +1 -1
- data/lib/live_cable/component/reactive_variables.rb +33 -5
- data/lib/live_cable/component/rendering.rb +30 -14
- data/lib/live_cable/component/streaming.rb +35 -4
- data/lib/live_cable/component.rb +4 -3
- data/lib/live_cable/connection/broadcasting.rb +15 -3
- data/lib/live_cable/connection/component_management.rb +12 -8
- data/lib/live_cable/connection/error_handling.rb +28 -7
- data/lib/live_cable/connection/messaging.rb +40 -52
- data/lib/live_cable/connection/state_management.rb +13 -16
- data/lib/live_cable/connection.rb +33 -0
- data/lib/live_cable/container.rb +17 -11
- data/lib/live_cable/defaults_signer.rb +95 -0
- data/lib/live_cable/delegation/array.rb +27 -1
- data/lib/live_cable/delegation/hash.rb +55 -1
- data/lib/live_cable/delegation/methods.rb +25 -1
- data/lib/live_cable/delegation/model.rb +12 -0
- data/lib/live_cable/delegator.rb +54 -6
- data/lib/live_cable/dispatch.rb +54 -0
- data/lib/live_cable/engine.rb +3 -1
- data/lib/live_cable/forbidden.rb +7 -0
- data/lib/live_cable/identity_digest.rb +27 -0
- data/lib/live_cable/model_observer.rb +37 -0
- data/lib/live_cable/observer_tracking.rb +10 -1
- data/lib/live_cable/predicate_builder.rb +13 -0
- data/lib/live_cable/render_context.rb +82 -8
- data/lib/live_cable/rendering/controller_renderer.rb +26 -0
- data/lib/live_cable/rendering/dependency_visitor.rb +68 -24
- data/lib/live_cable/rendering/handler.rb +1 -1
- data/lib/live_cable/rendering/method_analyzer.rb +18 -4
- data/lib/live_cable/rendering/method_collector.rb +13 -5
- data/lib/live_cable/rendering/partial.rb +13 -1
- data/lib/live_cable/rendering/partial_renderer.rb +59 -7
- data/lib/live_cable/rendering/render_result.rb +5 -0
- data/lib/live_cable/rendering/renderer.rb +18 -14
- data/lib/live_cable/rendering/view_buffer.rb +21 -0
- data/lib/live_cable/testing/render_state.rb +5 -3
- data/lib/live_cable/testing/test_channel.rb +6 -0
- data/lib/live_cable/testing/test_component.rb +3 -3
- data/lib/live_cable/testing.rb +9 -5
- data/lib/live_cable/version.rb +1 -1
- data/lib/live_cable.rb +32 -3
- metadata +9 -1
|
@@ -51,12 +51,21 @@ module LiveCable
|
|
|
51
51
|
observers.delete(observer)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# @api private
|
|
55
|
+
# @param other [ObserverTracking] starts notifying this object's observers too
|
|
56
|
+
# @return [void]
|
|
57
|
+
def share_live_cable_observers_with(other)
|
|
58
|
+
live_cable_observers.each do |variable, observers|
|
|
59
|
+
observers.each { |observer| other.add_live_cable_observer(observer, variable) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
def initialize_dup(other)
|
|
55
64
|
super
|
|
56
65
|
@live_cable_observers = {}
|
|
57
66
|
end
|
|
58
67
|
|
|
59
|
-
def initialize_clone(other)
|
|
68
|
+
def initialize_clone(other, **)
|
|
60
69
|
super
|
|
61
70
|
@live_cable_observers = {}
|
|
62
71
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
# Lets where(column: value) see the Array or Hash inside a reactive value
|
|
5
|
+
# instead of treating the wrapper as a single scalar.
|
|
6
|
+
module PredicateBuilder
|
|
7
|
+
def initialize(...)
|
|
8
|
+
super
|
|
9
|
+
|
|
10
|
+
register_handler(Delegator, ->(attribute, value) { build(attribute, value.__getobj__) })
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -30,16 +30,17 @@ module LiveCable
|
|
|
30
30
|
result
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# destroyed — their part simply didn't re-evaluate.
|
|
33
|
+
# Carried children stay out of #children, which broadcast_changeset reads as
|
|
34
|
+
# "rendered this cycle": a child there gets no render of its own that cycle.
|
|
36
35
|
#
|
|
37
|
-
# @param
|
|
36
|
+
# @param previous [RenderContext]
|
|
37
|
+
def inherit_skipped(previous)
|
|
38
|
+
@children_by_part.reverse_merge!(previous.children_by_part)
|
|
39
|
+
end
|
|
40
|
+
|
|
38
41
|
# @return [Array<LiveCable::Component>]
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
preserved.concat(part_children) unless @children_by_part.key?(part)
|
|
42
|
-
end
|
|
42
|
+
def owned_children
|
|
43
|
+
children_by_part.values.flatten.uniq
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def root?
|
|
@@ -78,9 +79,82 @@ module LiveCable
|
|
|
78
79
|
component.live_connection
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
# @param delegator [LiveCable::Delegator]
|
|
83
|
+
# @return [Object] the value the template sees
|
|
84
|
+
def unwrap(delegator)
|
|
85
|
+
value = delegator.__getobj__
|
|
86
|
+
|
|
87
|
+
unless tracked_values.key?(value)
|
|
88
|
+
tracked_values[value] = [delegator, []]
|
|
89
|
+
unindexed << delegator
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Maps a prop back to tracked values, so a child's changes still mark this
|
|
96
|
+
# component's variables, even through a collection built in the template.
|
|
97
|
+
def track(value)
|
|
98
|
+
return value unless Delegator.supported?(value)
|
|
99
|
+
|
|
100
|
+
while (delegator = unindexed.shift)
|
|
101
|
+
index_nested(delegator, delegator.__getobj__, [])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
delegator, path = tracked_values[value]
|
|
105
|
+
return path.reduce(delegator) { |nested, key| nested[key] } if delegator
|
|
106
|
+
|
|
107
|
+
sources = sources_within(value)
|
|
108
|
+
return value if sources.empty?
|
|
109
|
+
|
|
110
|
+
Delegator.new(value).tap do |wrapped|
|
|
111
|
+
sources.each_key { |source| source.share_live_cable_observers_with(wrapped) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
81
115
|
private
|
|
82
116
|
|
|
83
117
|
# @return [LiveCable::RenderContext, nil]
|
|
84
118
|
attr_reader :root
|
|
119
|
+
|
|
120
|
+
# @return [Hash{Object => Array(LiveCable::Delegator, Array)}] by identity
|
|
121
|
+
def tracked_values
|
|
122
|
+
@tracked_values ||= {}.compare_by_identity
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [Array<LiveCable::Delegator>]
|
|
126
|
+
def unindexed
|
|
127
|
+
@unindexed ||= []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def index_nested(delegator, value, path)
|
|
131
|
+
nested_entries(value).each do |key, child|
|
|
132
|
+
next if !Delegator.supported?(child) || tracked_values.key?(child)
|
|
133
|
+
|
|
134
|
+
tracked_values[child] = [delegator, path + [key]]
|
|
135
|
+
index_nested(delegator, child, path + [key])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [Hash{LiveCable::Delegator => true}] by identity
|
|
140
|
+
def sources_within(value, sources = {}.compare_by_identity, seen = {}.compare_by_identity)
|
|
141
|
+
nested_entries(value).map(&:last).each do |child|
|
|
142
|
+
next if !Delegator.supported?(child) || seen.key?(child)
|
|
143
|
+
|
|
144
|
+
seen[child] = true
|
|
145
|
+
delegator, = tracked_values[child]
|
|
146
|
+
delegator ? sources[delegator] = true : sources_within(child, sources, seen)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
sources
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def nested_entries(value)
|
|
153
|
+
case value
|
|
154
|
+
when Hash then value.each_pair
|
|
155
|
+
when Array then value.each_with_index.map { |child, index| [index, child] }
|
|
156
|
+
else []
|
|
157
|
+
end
|
|
158
|
+
end
|
|
85
159
|
end
|
|
86
160
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
module Rendering
|
|
5
|
+
# Builds the controller ApplicationController.renderer renders with, from
|
|
6
|
+
# the same env, so a connection can keep one and take a view from it per
|
|
7
|
+
# render instead of paying for a controller, request and response each time.
|
|
8
|
+
class ControllerRenderer < ActionController::Renderer
|
|
9
|
+
# @param controller_class [Class]
|
|
10
|
+
# @return [ActionController::Base]
|
|
11
|
+
def self.controller_for(controller_class)
|
|
12
|
+
self.for(controller_class, nil, controller_class.renderer.defaults).build_controller
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_controller
|
|
16
|
+
request = ActionDispatch::Request.new(env_for_request)
|
|
17
|
+
request.routes = controller._routes
|
|
18
|
+
|
|
19
|
+
controller.new.tap do |instance|
|
|
20
|
+
instance.set_request!(request)
|
|
21
|
+
instance.set_response!(controller.make_response!(request))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
3
5
|
module LiveCable
|
|
4
6
|
module Rendering
|
|
5
7
|
class DependencyVisitor < Prism::Visitor
|
|
8
|
+
# @return [Array<Symbol>]
|
|
9
|
+
attr_reader :variable_calls
|
|
10
|
+
|
|
6
11
|
# @return [Array<Symbol>]
|
|
7
12
|
attr_reader :local_reads
|
|
8
13
|
|
|
@@ -14,32 +19,40 @@ module LiveCable
|
|
|
14
19
|
|
|
15
20
|
def initialize
|
|
16
21
|
super
|
|
22
|
+
@variable_calls = []
|
|
17
23
|
@local_reads = []
|
|
18
24
|
@local_writes = []
|
|
19
25
|
@component_method_calls = Set.new
|
|
26
|
+
@scope_depth = 0
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
def visit_block_node(node) = nested_scope { super }
|
|
30
|
+
|
|
31
|
+
def visit_lambda_node(node) = nested_scope { super }
|
|
32
|
+
|
|
33
|
+
# Prism never resolves a name past a def, so nothing inside one counts as top level
|
|
34
|
+
def visit_def_node(node) = nested_scope { super }
|
|
35
|
+
|
|
36
|
+
# Track reads of top-level locals
|
|
23
37
|
# @param node [Prism::LocalVariableReadNode]
|
|
24
38
|
# @return [void]
|
|
25
39
|
def visit_local_variable_read_node(node)
|
|
26
|
-
@local_reads |= [node.name]
|
|
40
|
+
@local_reads |= [node.name] if top_level?(node)
|
|
27
41
|
super
|
|
28
42
|
end
|
|
29
43
|
|
|
30
|
-
# Track
|
|
31
|
-
#
|
|
44
|
+
# Track argument-less receiverless calls (e.g., `foo`, `foo()` or the value
|
|
45
|
+
# of a `foo:` hash shorthand) and calls on component or self (e.g.,
|
|
46
|
+
# `component.foo`), which read the component too
|
|
32
47
|
# @param node [Prism::CallNode]
|
|
33
48
|
# @return [void]
|
|
34
49
|
def visit_call_node(node)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# Track component.method_name calls
|
|
39
|
-
if component_receiver?(node.receiver)
|
|
40
|
-
@component_method_calls << node.name
|
|
50
|
+
if node.variable_call? || bare_call?(node) || explicit_component_receiver?(node.receiver)
|
|
51
|
+
@variable_calls |= [node.name]
|
|
41
52
|
end
|
|
42
53
|
|
|
54
|
+
@component_method_calls << node.name if component_receiver?(node.receiver)
|
|
55
|
+
|
|
43
56
|
super
|
|
44
57
|
end
|
|
45
58
|
|
|
@@ -47,7 +60,15 @@ module LiveCable
|
|
|
47
60
|
# @param node [Prism::LocalVariableWriteNode]
|
|
48
61
|
# @return [void]
|
|
49
62
|
def visit_local_variable_write_node(node)
|
|
50
|
-
@local_writes |= [node.name]
|
|
63
|
+
@local_writes |= [node.name] if top_level?(node)
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Track multiple assignment, rescue and pattern targets
|
|
68
|
+
# @param node [Prism::LocalVariableTargetNode]
|
|
69
|
+
# @return [void]
|
|
70
|
+
def visit_local_variable_target_node(node)
|
|
71
|
+
@local_writes |= [node.name] if top_level?(node)
|
|
51
72
|
super
|
|
52
73
|
end
|
|
53
74
|
|
|
@@ -55,8 +76,7 @@ module LiveCable
|
|
|
55
76
|
# @param node [Prism::LocalVariableOperatorWriteNode]
|
|
56
77
|
# @return [void]
|
|
57
78
|
def visit_local_variable_operator_write_node(node)
|
|
58
|
-
|
|
59
|
-
@local_reads |= [node.name] # Reads before writing
|
|
79
|
+
read_and_write(node)
|
|
60
80
|
super
|
|
61
81
|
end
|
|
62
82
|
|
|
@@ -64,8 +84,7 @@ module LiveCable
|
|
|
64
84
|
# @param node [Prism::LocalVariableAndWriteNode]
|
|
65
85
|
# @return [void]
|
|
66
86
|
def visit_local_variable_and_write_node(node)
|
|
67
|
-
|
|
68
|
-
@local_reads |= [node.name]
|
|
87
|
+
read_and_write(node)
|
|
69
88
|
super
|
|
70
89
|
end
|
|
71
90
|
|
|
@@ -73,24 +92,49 @@ module LiveCable
|
|
|
73
92
|
# @param node [Prism::LocalVariableOrWriteNode]
|
|
74
93
|
# @return [void]
|
|
75
94
|
def visit_local_variable_or_write_node(node)
|
|
76
|
-
|
|
77
|
-
@local_reads |= [node.name]
|
|
95
|
+
read_and_write(node)
|
|
78
96
|
super
|
|
79
97
|
end
|
|
80
98
|
|
|
81
99
|
private
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
def nested_scope
|
|
102
|
+
@scope_depth += 1
|
|
103
|
+
yield
|
|
104
|
+
ensure
|
|
105
|
+
@scope_depth -= 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Prism's depth is how many scopes up it found the local
|
|
109
|
+
def top_level?(node)
|
|
110
|
+
node.depth == @scope_depth
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read_and_write(node)
|
|
114
|
+
return unless top_level?(node)
|
|
115
|
+
|
|
116
|
+
@local_reads |= [node.name]
|
|
117
|
+
@local_writes |= [node.name]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @param node [Prism::CallNode]
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
def bare_call?(node)
|
|
123
|
+
node.receiver.nil? && node.arguments.nil? && node.block.nil?
|
|
124
|
+
end
|
|
125
|
+
|
|
84
126
|
# @param receiver [Prism::Node, nil]
|
|
85
127
|
# @return [Boolean]
|
|
86
128
|
def component_receiver?(receiver)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# Ignore these because they're not component methods, just output methods
|
|
91
|
-
return false if receiver.try(:name) == :@output_buffer
|
|
129
|
+
receiver.nil? || explicit_component_receiver?(receiver)
|
|
130
|
+
end
|
|
92
131
|
|
|
93
|
-
|
|
132
|
+
# Part code runs on the renderer, which hands unknown names to the component,
|
|
133
|
+
# so `self.foo` reads the component just as `component.foo` does
|
|
134
|
+
# @param receiver [Prism::Node, nil]
|
|
135
|
+
# @return [Boolean]
|
|
136
|
+
def explicit_component_receiver?(receiver)
|
|
137
|
+
return true if receiver.is_a?(Prism::SelfNode)
|
|
94
138
|
return false unless receiver.try(:name) == :component
|
|
95
139
|
|
|
96
140
|
receiver.is_a?(Prism::CallNode) || receiver.is_a?(Prism::LocalVariableReadNode)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
3
5
|
module LiveCable
|
|
4
6
|
module Rendering
|
|
5
7
|
# Analyzes component methods to track their dependencies on reactive variables
|
|
@@ -19,11 +21,15 @@ module LiveCable
|
|
|
19
21
|
def analyze_all_methods
|
|
20
22
|
return dependencies if analyzed
|
|
21
23
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
+
# Classes built with Class.new have no source file to analyze; see analyzable?
|
|
25
|
+
class_name = component_class.name
|
|
26
|
+
file_path = class_name && Object.const_source_location(class_name)&.first
|
|
24
27
|
|
|
25
|
-
file_path
|
|
26
|
-
|
|
28
|
+
unless file_path && File.exist?(file_path)
|
|
29
|
+
@analyzed = true
|
|
30
|
+
@analyzable = false
|
|
31
|
+
return dependencies
|
|
32
|
+
end
|
|
27
33
|
|
|
28
34
|
# Parse the entire file once
|
|
29
35
|
source_code = File.read(file_path)
|
|
@@ -35,9 +41,17 @@ module LiveCable
|
|
|
35
41
|
|
|
36
42
|
@dependencies = collector.dependencies
|
|
37
43
|
@analyzed = true
|
|
44
|
+
@analyzable = true
|
|
38
45
|
dependencies
|
|
39
46
|
end
|
|
40
47
|
|
|
48
|
+
# @return [Boolean] false when the class has no source file, so method
|
|
49
|
+
# dependencies are unknown and callers must assume any change matters
|
|
50
|
+
def analyzable?
|
|
51
|
+
analyze_all_methods unless analyzed
|
|
52
|
+
@analyzable
|
|
53
|
+
end
|
|
54
|
+
|
|
41
55
|
# Get dependencies for a specific method (analyzes all if not done yet)
|
|
42
56
|
# @param method_name [Symbol] The method name
|
|
43
57
|
# @return [Hash, nil] { methods: Set, reactive_vars: Set }
|
|
@@ -15,7 +15,7 @@ module LiveCable
|
|
|
15
15
|
|
|
16
16
|
@component_class = component_class
|
|
17
17
|
@dependencies = {}
|
|
18
|
-
@reactive_vars = component_class.all_reactive_variables
|
|
18
|
+
@reactive_vars = component_class.all_reactive_variables | component_class.shared_variables
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Visit each method definition
|
|
@@ -24,6 +24,8 @@ module LiveCable
|
|
|
24
24
|
def visit_def_node(node)
|
|
25
25
|
method_name = node.name
|
|
26
26
|
|
|
27
|
+
return if node.receiver
|
|
28
|
+
|
|
27
29
|
# Skip private/internal methods
|
|
28
30
|
return super if %i[render render_in].include?(method_name)
|
|
29
31
|
|
|
@@ -31,14 +33,20 @@ module LiveCable
|
|
|
31
33
|
visitor = MethodDependencyVisitor.new(component_class, reactive_vars)
|
|
32
34
|
visitor.visit(node)
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# Nested and sibling classes in the same file share this table, so a
|
|
37
|
+
# clashing name may only add dependencies, never replace them.
|
|
38
|
+
entry = @dependencies[method_name] ||= { methods: Set.new, reactive_vars: Set.new }
|
|
39
|
+
entry[:methods].merge(visitor.method_calls)
|
|
40
|
+
entry[:reactive_vars].merge(visitor.reactive_var_reads)
|
|
38
41
|
|
|
39
42
|
super
|
|
40
43
|
end
|
|
41
44
|
|
|
45
|
+
# Methods in `class << self` are never instance methods a template calls
|
|
46
|
+
# @param node [Prism::SingletonClassNode]
|
|
47
|
+
# @return [void]
|
|
48
|
+
def visit_singleton_class_node(node); end
|
|
49
|
+
|
|
42
50
|
private
|
|
43
51
|
|
|
44
52
|
# @return [Class]
|
|
@@ -5,9 +5,10 @@ require_relative 'partial_renderer'
|
|
|
5
5
|
module LiveCable
|
|
6
6
|
module Rendering
|
|
7
7
|
class Partial
|
|
8
|
-
def initialize(parts, metadata)
|
|
8
|
+
def initialize(parts, metadata, identifier)
|
|
9
9
|
@parts = parts
|
|
10
10
|
@metadata = metadata
|
|
11
|
+
@identifier = identifier
|
|
11
12
|
@renderer_class = build_renderer_class
|
|
12
13
|
end
|
|
13
14
|
|
|
@@ -16,6 +17,14 @@ module LiveCable
|
|
|
16
17
|
renderer_class.new(component, parts, view_context)
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
# ActionView calls this when a template is rendered as a partial or a plain
|
|
21
|
+
# template; only Component#render_in can render a Partial.
|
|
22
|
+
def to_s
|
|
23
|
+
raise LiveCable::Error,
|
|
24
|
+
"#{identifier} is a component template and can only be rendered by its component. " \
|
|
25
|
+
'Render the component with live(...) or render(component), or make the partial a .html.erb template.'
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
private
|
|
20
29
|
|
|
21
30
|
# @return [Array]
|
|
@@ -24,6 +33,9 @@ module LiveCable
|
|
|
24
33
|
# @return [Array]
|
|
25
34
|
attr_reader :metadata
|
|
26
35
|
|
|
36
|
+
# @return [String]
|
|
37
|
+
attr_reader :identifier
|
|
38
|
+
|
|
27
39
|
# @return [Class<PartialRenderer>]
|
|
28
40
|
attr_reader :renderer_class
|
|
29
41
|
|
|
@@ -11,6 +11,7 @@ module LiveCable
|
|
|
11
11
|
@component = component
|
|
12
12
|
@parts = parts
|
|
13
13
|
@view_context = view_context
|
|
14
|
+
bind_shadowed_names
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def render_changes(changes = :all)
|
|
@@ -25,15 +26,19 @@ module LiveCable
|
|
|
25
26
|
@method_deps_cache = {} # Cache method dependency expansion for this render cycle
|
|
26
27
|
parts.each_with_index.with_object([]) do |item, acc|
|
|
27
28
|
part, index = item
|
|
28
|
-
result = view_context.render_part(index) {
|
|
29
|
+
result = view_context.render_part(index) { __send__("render_part_#{index}", changes) }
|
|
29
30
|
acc << result unless part[0] == :code
|
|
30
31
|
end
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
# Pass through renders to the view context, otherwise method_missing will pass
|
|
34
35
|
# to the component instead since it also has a render method
|
|
35
|
-
def render(
|
|
36
|
-
view_context.render(*)
|
|
36
|
+
def render(*, &block)
|
|
37
|
+
return view_context.render(*) unless block
|
|
38
|
+
|
|
39
|
+
view_context.render(*) do |*args|
|
|
40
|
+
with_view_buffer { block.call(*args) }
|
|
41
|
+
end
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def mark_locals_dirty(locals)
|
|
@@ -80,6 +85,8 @@ module LiveCable
|
|
|
80
85
|
# Always render on template switch (all dynamic parts must render)
|
|
81
86
|
return false if changes == :dynamic
|
|
82
87
|
|
|
88
|
+
return false if component_method_calls.any? && !component.class.method_dependencies_analyzer.analyzable?
|
|
89
|
+
|
|
83
90
|
# Expand component.method_name calls to their transitive dependencies
|
|
84
91
|
expanded_deps = expand_component_method_dependencies(component_method_calls)
|
|
85
92
|
all_component_deps = component_dependencies | expanded_deps
|
|
@@ -94,17 +101,18 @@ module LiveCable
|
|
|
94
101
|
true
|
|
95
102
|
end
|
|
96
103
|
|
|
97
|
-
def method_missing(method,
|
|
98
|
-
|
|
104
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
105
|
+
# A local is only ever a bare name; with arguments it's a method call, as in plain ERB
|
|
106
|
+
if locals.key?(method) && args.empty? && kwargs.empty? && block.nil?
|
|
99
107
|
return locals[method]
|
|
100
108
|
end
|
|
101
109
|
|
|
102
110
|
if component.respond_to?(method)
|
|
103
|
-
return component.public_send(method,
|
|
111
|
+
return view_context.live_cable_unwrap(component.public_send(method, *args, **kwargs, &block))
|
|
104
112
|
end
|
|
105
113
|
|
|
106
114
|
if view_context.respond_to?(method)
|
|
107
|
-
return view_context.public_send(method,
|
|
115
|
+
return forwarding_to_view_buffer { view_context.public_send(method, *args, **kwargs, &block) }
|
|
108
116
|
end
|
|
109
117
|
|
|
110
118
|
super
|
|
@@ -137,9 +145,53 @@ module LiveCable
|
|
|
137
145
|
view_context.with_output_buffer(@output_buffer, &).to_s
|
|
138
146
|
end
|
|
139
147
|
|
|
148
|
+
# Template blocks write to this renderer's @output_buffer, but a partial
|
|
149
|
+
# yields them while its own buffer is the one capturing.
|
|
150
|
+
def with_view_buffer
|
|
151
|
+
buffer = @output_buffer
|
|
152
|
+
@output_buffer = view_context.output_buffer
|
|
153
|
+
yield
|
|
154
|
+
ensure
|
|
155
|
+
@output_buffer = buffer
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def forwarding_to_view_buffer
|
|
159
|
+
buffer = @output_buffer
|
|
160
|
+
@output_buffer = ViewBuffer.new(view_context)
|
|
161
|
+
yield
|
|
162
|
+
ensure
|
|
163
|
+
@output_buffer = buffer
|
|
164
|
+
end
|
|
165
|
+
|
|
140
166
|
def store_local(name, value)
|
|
141
167
|
locals[name] = value
|
|
142
168
|
end
|
|
169
|
+
|
|
170
|
+
# Kernel and Object names never reach method_missing. Bind the ones the component
|
|
171
|
+
# or view context define themselves; a wrapper falling back to Kernel breaks lambda.
|
|
172
|
+
def bind_shadowed_names
|
|
173
|
+
names = self.class.metadata.compact.flat_map { |part| part[:component_method_calls] || [] }.uniq
|
|
174
|
+
names = names.select { |name| Object.method_defined?(name) || Object.private_method_defined?(name) }
|
|
175
|
+
|
|
176
|
+
# The renderer's respond_to? already checks locals, the component and the view context.
|
|
177
|
+
(names - [:respond_to?]).each do |name|
|
|
178
|
+
target = [component, view_context].find do |object|
|
|
179
|
+
if Object.private_method_defined?(name)
|
|
180
|
+
object.respond_to?(name)
|
|
181
|
+
else
|
|
182
|
+
object.class.public_method_defined?(name) &&
|
|
183
|
+
object.class.instance_method(name).owner != Object.instance_method(name).owner
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
next unless target
|
|
187
|
+
|
|
188
|
+
define_singleton_method(name) do |*args, **kwargs, &block|
|
|
189
|
+
next target.public_send(name, *args, **kwargs, &block) if target.equal?(component)
|
|
190
|
+
|
|
191
|
+
forwarding_to_view_buffer { target.public_send(name, *args, **kwargs, &block) }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
143
195
|
end
|
|
144
196
|
end
|
|
145
197
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
3
5
|
module LiveCable
|
|
4
6
|
module Rendering
|
|
5
7
|
class Renderer < ::Herb::Engine
|
|
6
8
|
# rubocop:disable Lint/MissingSuper
|
|
7
|
-
def initialize
|
|
9
|
+
def initialize(identifier = nil)
|
|
10
|
+
@identifier = identifier
|
|
8
11
|
@newline_pending = 0
|
|
9
12
|
@parts = []
|
|
10
13
|
@src = +''
|
|
@@ -20,11 +23,14 @@ module LiveCable
|
|
|
20
23
|
|
|
21
24
|
def src
|
|
22
25
|
metadata = build_metadata
|
|
23
|
-
"::LiveCable::Rendering::Partial.new(#{parts.inspect}, #{metadata.inspect})"
|
|
26
|
+
"::LiveCable::Rendering::Partial.new(#{parts.inspect}, #{metadata.inspect}, #{identifier.inspect})"
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
private
|
|
27
30
|
|
|
31
|
+
# @return [String, nil]
|
|
32
|
+
attr_reader :identifier
|
|
33
|
+
|
|
28
34
|
# @return [Integer]
|
|
29
35
|
attr_reader :newline_pending
|
|
30
36
|
|
|
@@ -43,22 +49,20 @@ module LiveCable
|
|
|
43
49
|
parts.map do |type, code|
|
|
44
50
|
next nil if type == :static || code.nil? || code.empty?
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
local_check_code = +''
|
|
49
|
-
|
|
50
|
-
locals_defined_here.each do |local|
|
|
51
|
-
local_check_code << "store_local(:#{local}, #{local}) if defined?(#{local})\n"
|
|
52
|
-
end
|
|
52
|
+
# Earlier parts' locals are in scope, as they would be in one ERB method body
|
|
53
|
+
parsed = Prism.parse(code, scopes: [accumulated_locals]).value
|
|
53
54
|
|
|
54
55
|
visitor = DependencyVisitor.new
|
|
55
56
|
visitor.visit(parsed)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
local_check_code = +''
|
|
59
|
+
|
|
60
|
+
visitor.local_writes.each do |local|
|
|
61
|
+
local_check_code << "store_local(:#{local}, #{local}) if defined?(#{local})\n"
|
|
62
|
+
end
|
|
59
63
|
|
|
60
|
-
#
|
|
61
|
-
local_dependencies = visitor.local_reads & accumulated_locals
|
|
64
|
+
# A write that doesn't run, such as one in a false `if`, keeps the earlier value
|
|
65
|
+
local_dependencies = (visitor.local_reads | visitor.local_writes) & accumulated_locals
|
|
62
66
|
|
|
63
67
|
# Track component.method_name calls separately for runtime expansion
|
|
64
68
|
component_method_calls = visitor.component_method_calls.to_a
|
|
@@ -69,7 +73,7 @@ module LiveCable
|
|
|
69
73
|
{
|
|
70
74
|
type:,
|
|
71
75
|
code:,
|
|
72
|
-
component_dependencies:
|
|
76
|
+
component_dependencies: visitor.variable_calls - [:component],
|
|
73
77
|
component_method_calls:,
|
|
74
78
|
local_dependencies:,
|
|
75
79
|
defines_locals: visitor.local_writes,
|