diff --git a/app/channels/stimulus_reflex/channel.rb b/app/channels/stimulus_reflex/channel.rb index 569d3fe3..15a816df 100644 --- a/app/channels/stimulus_reflex/channel.rb +++ b/app/channels/stimulus_reflex/channel.rb @@ -98,6 +98,11 @@ def receive(data) private def delegate_call_to_reflex(reflex) + if reflex.cable_ready + reflex.element.cable_ready = reflex.cable_ready if reflex.element + reflex.controller_element.cable_ready = reflex.cable_ready if reflex.controller_element + end + method_name = reflex_data.method_name arguments = reflex_data.arguments method = reflex.method(method_name) diff --git a/lib/stimulus_reflex/element.rb b/lib/stimulus_reflex/element.rb index 5b9bd3f5..6477b72e 100644 --- a/lib/stimulus_reflex/element.rb +++ b/lib/stimulus_reflex/element.rb @@ -6,13 +6,17 @@ class StimulusReflex::Element < OpenStruct include StimulusReflex::AttributeBuilder - attr_reader :attrs, :dataset + attr_reader :attrs, :dataset, :selector, :cable_ready + attr_writer :cable_ready alias_method :data_attributes, :dataset delegate :signed, :unsigned, :numeric, :boolean, :data_attrs, to: :dataset + delegate :broadcast, to: :cable_ready + + def initialize(data = {}, selector: nil) + @selector = selector - def initialize(data = {}) @attrs = HashWithIndifferentAccess.new(data["attrs"] || {}) @dataset = StimulusReflex::Dataset.new(data) @@ -29,4 +33,21 @@ def to_dom_id "##{id}" end + + def method_missing(method_name, *arguments, &block) + if cable_ready.respond_to?(method_name) + xpath = selector ? selector.starts_with?("//") : false + args = arguments.first.to_h.reverse_merge(selector: selector, xpath: xpath) + + cable_ready.send(method_name.to_sym, args) + + cable_ready + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + cable_ready.respond_to?(method_name) || super + end end diff --git a/lib/stimulus_reflex/reflex.rb b/lib/stimulus_reflex/reflex.rb index 259617f4..368fd562 100644 --- a/lib/stimulus_reflex/reflex.rb +++ b/lib/stimulus_reflex/reflex.rb @@ -9,13 +9,14 @@ class StimulusReflex::Reflex class VersionMismatchError < StandardError; end prepend StimulusReflex::CableReadiness + include ActiveSupport::Rescuable include StimulusReflex::Callbacks include ActionView::Helpers::TagHelper include CableReady::Identifiable attr_accessor :payload, :headers - attr_reader :channel, :url, :element, :selectors, :method_name, :broadcaster, :client_attributes, :logger + attr_reader :channel, :url, :element, :controller_element, :selectors, :method_name, :broadcaster, :client_attributes, :logger alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name @@ -25,10 +26,11 @@ class VersionMismatchError < StandardError; end # TODO remove xpath_controller and xpath_element for v4 delegate :id, :tab_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, :version, :suppress_logging, to: :client_attributes - def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {}) + def initialize(channel, url: nil, element: nil, controller_element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {}) @channel = channel @url = url @element = element + @controller_element = controller_element @selectors = selectors @method_name = method_name @params = params diff --git a/lib/stimulus_reflex/reflex_data.rb b/lib/stimulus_reflex/reflex_data.rb index c3696d01..115e29d6 100644 --- a/lib/stimulus_reflex/reflex_data.rb +++ b/lib/stimulus_reflex/reflex_data.rb @@ -36,7 +36,11 @@ def url end def element - StimulusReflex::Element.new(data) + @element ||= StimulusReflex::Element.new(data, selector: xpath_element) + end + + def controller_element + @controller_element ||= StimulusReflex::Element.new(data, selector: xpath_controller) end def permanent_attribute_name diff --git a/lib/stimulus_reflex/reflex_factory.rb b/lib/stimulus_reflex/reflex_factory.rb index eb5d0b68..9bee0918 100644 --- a/lib/stimulus_reflex/reflex_factory.rb +++ b/lib/stimulus_reflex/reflex_factory.rb @@ -9,6 +9,7 @@ def create_reflex_from_data(channel, reflex_data) reflex_class.new(channel, url: reflex_data.url, element: reflex_data.element, + controller_element: reflex_data.controller_element, selectors: reflex_data.selectors, method_name: reflex_data.method_name, params: reflex_data.params, diff --git a/test/broadcasters/broadcaster_test_case.rb b/test/broadcasters/broadcaster_test_case.rb index 72716a31..ce7cbd5a 100644 --- a/test/broadcasters/broadcaster_test_case.rb +++ b/test/broadcasters/broadcaster_test_case.rb @@ -5,30 +5,6 @@ class StimulusReflex::BroadcasterTestCase < ActionCable::Channel::TestCase tests StimulusReflex::Channel - def assert_broadcast_on(stream, data, &block) - serialized_msg = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data)) - - new_messages = broadcasts(stream) - if block - old_messages = new_messages - clear_messages(stream) - - yield - new_messages = broadcasts(stream) - clear_messages(stream) - - (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) } - end - - message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg } - - unless message - puts "\n\nActual: #{ActiveSupport::JSON.decode(new_messages.first)}\n\nExpected: #{data}\n\n" - end - - assert message, "No messages sent with #{data} to #{stream}" - end - setup do stub_connection(session_id: SecureRandom.uuid) def connection.env diff --git a/test/reflex_element_test.rb b/test/reflex_element_test.rb new file mode 100644 index 00000000..6d74a145 --- /dev/null +++ b/test/reflex_element_test.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "mocha/minitest" + +class StimulusReflex::ReflexElementTest < ActionCable::Channel::TestCase + tests StimulusReflex::Channel + + setup do + stub_connection(session_id: SecureRandom.uuid) + def connection.env + @env ||= {} + end + + @element = StimulusReflex::Element.new({}, selector: "#element-selector") + + @reflex = StimulusReflex::Reflex.new(subscribe, url: "https://test.stimulusreflex.com", client_attributes: {id: "666", version: StimulusReflex::VERSION}) + @cable_ready = StimulusReflex::CableReadyChannels.new(@reflex) + @element.cable_ready = @cable_ready + end + + def build_payload(operations = []) + { + "cableReady" => true, + "operations" => Array.wrap(operations), + "version" => CableReady::VERSION + } + end + + test "broadcasts updates using element.broadcast" do + expected = build_payload( + {"selector" => "#element-selector", "xpath" => false, "html" => "
Some HTML
", "reflexId" => "666", "operation" => "innerHtml"} + ) + + assert_broadcast_on(@reflex.stream_name, expected) do + @element.inner_html(html: "Some HTML
") + @element.broadcast + end + end + + test "selector can be overwritten" do + expected = build_payload( + {"selector" => "#overwritten", "xpath" => false, "html" => "Some HTML
", "reflexId" => "666", "operation" => "innerHtml"} + ) + + assert_broadcast_on(@reflex.stream_name, expected) do + @element.inner_html(html: "Some HTML
", selector: "#overwritten").broadcast + end + end + + test "broadcasts using element.broadcast chained" do + expected = build_payload [ + {"selector" => "#element-selector", "xpath" => false, "html" => "Some HTML
", "reflexId" => "666", "operation" => "innerHtml"}, + {"name" => "abc", "detail" => {"some" => "key"}, "reflexId" => "666", "selector" => "#element-selector", "operation" => "dispatchEvent"} + ] + + assert_broadcast_on(@reflex.stream_name, expected) do + @element.inner_html(html: "Some HTML
").dispatch_event(name: "abc", detail: {some: "key"}).broadcast + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0bb232e6..88b3825e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -67,6 +67,30 @@ def connection_gid(ids) end end +def assert_broadcast_on(stream, data, &block) + serialized_msg = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data)) + + new_messages = broadcasts(stream) + if block + old_messages = new_messages + clear_messages(stream) + + yield + new_messages = broadcasts(stream) + clear_messages(stream) + + (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) } + end + + message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg } + + unless message + puts "\n\nActual: #{ActiveSupport::JSON.decode(new_messages.first)}\n\nExpected: #{data}\n\n" + end + + assert message, "No messages sent with #{data} to #{stream}" +end + StimulusReflex.configuration.parent_channel = "ActionCable::Channel::Base" ActionCable::Server::Base.config.cable = {adapter: "test"} ActionCable::Server::Base.config.logger = Logger.new(nil)