Skip to content

Commit

Permalink
chainable selectors (#107)
Browse files Browse the repository at this point in the history
Co-authored-by: Marco Roth <marco.roth@intergga.ch>
  • Loading branch information
leastbad and marcoroth authored Apr 9, 2021
1 parent aee1d37 commit 7cea7eb
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 25 deletions.
10 changes: 10 additions & 0 deletions lib/cable_ready.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
# frozen_string_literal: true

require "rails/engine"
require "active_record"
require "action_view"
require "active_support/all"
require "thread/local"
require "monitor"
require "observer"
require "singleton"
require "cable_ready/version"
require "cable_ready/identifiable"
require "cable_ready/operation_builder"
require "cable_ready/config"
require "cable_ready/broadcaster"
require "cable_ready/channel"
require "cable_ready/channels"
require "cable_ready/cable_car"

module CableReady
class Engine < Rails::Engine
Expand Down
8 changes: 1 addition & 7 deletions lib/cable_ready/broadcaster.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# frozen_string_literal: true

require_relative "channels"
require_relative "cable_car"

module CableReady
module Broadcaster
include Identifiable
extend ::ActiveSupport::Concern

def cable_ready
Expand All @@ -14,9 +12,5 @@ def cable_ready
def cable_car
CableReady::CableCar.instance
end

def dom_id(record, prefix = nil)
"##{ActionView::RecordIdentifier.dom_id(record, prefix)}"
end
end
end
2 changes: 0 additions & 2 deletions lib/cable_ready/cable_car.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

require "thread/local"

module CableReady
class CableCar < OperationBuilder
extend Thread::Local
Expand Down
3 changes: 0 additions & 3 deletions lib/cable_ready/channels.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# frozen_string_literal: true

require "thread/local"
require_relative "channel"

module CableReady
# This class is a thread local singleton: CableReady::Channels.instance
# SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started
Expand Down
4 changes: 0 additions & 4 deletions lib/cable_ready/config.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# frozen_string_literal: true

require "monitor"
require "observer"
require "singleton"

module CableReady
# This class is a process level singleton shared by all threads: CableReady::Config.instance
class Config
Expand Down
19 changes: 19 additions & 0 deletions lib/cable_ready/identifiable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module CableReady
module Identifiable
def dom_id(record, prefix = nil)
prefix = prefix.to_s.strip if prefix

id = if record.is_a?(ActiveRecord::Relation)
[prefix, record.model_name.plural].compact.join("_")
elsif record.is_a?(ActiveRecord::Base)
ActionView::RecordIdentifier.dom_id(record, prefix)
else
[prefix, record.to_s.strip].compact.join("_")
end

"##{id}".squeeze("#").strip
end
end
end
25 changes: 19 additions & 6 deletions lib/cable_ready/operation_builder.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# frozen_string_literal: true

module CableReady
class OperationBuilder
attr_reader :identifier
include Identifiable
attr_reader :identifier, :previous_selector

def self.finalizer_for(identifier)
proc {
Expand All @@ -20,9 +23,18 @@ def initialize(identifier)

def add_operation_method(name)
return if respond_to?(name)

singleton_class.public_send :define_method, name, ->(options = {}) {
@enqueued_operations[name.to_s] << options.stringify_keys
singleton_class.public_send :define_method, name, ->(*args) {
selector, options = nil, args.first || {} # 1 or 0 params
selector, options = options, {} unless options.is_a?(Hash) # swap if only selector provided
selector, options = args[0, 2] if args.many? # 2 or more params
options.stringify_keys!
options["selector"] = selector if selector && options.exclude?("selector")
options["selector"] = previous_selector if previous_selector && options.exclude?("selector")
if options.include?("selector")
@previous_selector = options["selector"]
options["selector"] = previous_selector.is_a?(ActiveRecord::Base) || previous_selector.is_a?(ActiveRecord::Relation) ? dom_id(previous_selector) : previous_selector
end
@enqueued_operations[name.to_s] << options
self
}
end
Expand All @@ -37,9 +49,9 @@ def apply!(operations = "{}")
rescue JSON::ParserError
{}
end
operations.each do |key, operation|
operations.each do |name, operation|
operation.each do |enqueued_operation|
@enqueued_operations[key.to_s] << enqueued_operation
@enqueued_operations[name.to_s] << enqueued_operation
end
end
self
Expand All @@ -51,6 +63,7 @@ def operations_payload

def reset!
@enqueued_operations = Hash.new { |hash, key| hash[key] = [] }
@previous_selector = nil
end
end
end
75 changes: 75 additions & 0 deletions test/lib/cable_ready/identifiable_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

require "test_helper"
require_relative "../../../lib/cable_ready"

class User
include ActiveModel::Model

attr_accessor :id
end

class CableReady::IdentifiableTest < ActiveSupport::TestCase
include CableReady::Identifiable

test "should handle nil" do
assert_equal "#", dom_id(nil)
end

test "should work with strings" do
assert_equal "#users", dom_id("users")
assert_equal "#users", dom_id("users ")
assert_equal "#users", dom_id(" users ")
end

test "should work with symbols" do
assert_equal "#users", dom_id(:users)
assert_equal "#active_users", dom_id(:active_users)
end

test "should just return one hash" do
assert_equal "#users", dom_id("users")
assert_equal "#users", dom_id("#users")
assert_equal "#users", dom_id("##users")
end

test "should strip prefixes" do
assert_equal "#active_users", dom_id(" users ", " active ")
assert_equal "#all_active_users", dom_id(" users ", " all_active ")
end

test "should not include provided prefix if prefix is nil" do
assert_equal "#users", dom_id("users", nil)
end

test "should work with ActiveRecord::Relation" do
relation = mock("ActiveRecord::Relation")

relation.stubs(:is_a?).with(ActiveRecord::Relation).returns(true).at_least_once
relation.stubs(:is_a?).with(ActiveRecord::Base).never
relation.stubs(:model_name).returns(OpenStruct.new(plural: "users"))

assert_equal "#users", dom_id(relation)
assert_equal "#users", dom_id(relation, nil)
assert_equal "#active_users", dom_id(relation, "active")
end

test "should work with ActiveRecord::Base" do
User.any_instance.stubs(:is_a?).with(ActiveRecord::Relation).returns(false)
User.any_instance.stubs(:is_a?).with(ActiveRecord::Base).returns(true)

assert_equal "#new_user", dom_id(User.new(id: nil))

user = User.new(id: 42)

assert_equal "#user_42", dom_id(user)
assert_equal "#user_42", dom_id(user, nil)
assert_equal "#all_active_user_42", dom_id(user, "all_active")

user = User.new(id: 99)

assert_equal "#user_99", dom_id(user)
assert_equal "#user_99", dom_id(user, nil)
assert_equal "#all_active_user_99", dom_id(user, "all_active")
end
end
62 changes: 59 additions & 3 deletions test/lib/cable_ready/operation_builder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,64 @@ class CableReady::OperationBuilderTest < ActiveSupport::TestCase
end

test "operations payload should camelize keys" do
@operation_builder.add_operation_method("foobar")
@operation_builder.foobar({beep_boop: "passed_option"})
assert_equal({"foobar" => [{"beepBoop" => "passed_option"}]}, @operation_builder.operations_payload)
@operation_builder.add_operation_method("foo_bar")
@operation_builder.foo_bar({beep_boop: "passed_option"})
assert_equal({"fooBar" => [{"beepBoop" => "passed_option"}]}, @operation_builder.operations_payload)
end

test "should take first argument as selector" do
@operation_builder.add_operation_method("inner_html")

@operation_builder.inner_html("#smelly", html: "<span>I rock</span>")

operations = {
"innerHtml" => [{"html" => "<span>I rock</span>", "selector" => "#smelly"}]
}

assert_equal(operations, @operation_builder.operations_payload)
end

test "should use previously passed selector in next operation" do
@operation_builder.add_operation_method("inner_html")
@operation_builder.add_operation_method("set_focus")

@operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>")

operations = {
"setFocus" => [{"selector" => "#smelly"}],
"innerHtml" => [{"html" => "<span>I rock</span>", "selector" => "#smelly"}]
}

assert_equal(operations, @operation_builder.operations_payload)
end

test "should clear previous_selector after calling reset!" do
@operation_builder.add_operation_method("inner_html")
@operation_builder.inner_html(selector: "#smelly", html: "<span>I rock</span>")

@operation_builder.reset!

@operation_builder.inner_html(html: "<span>winning</span>")

assert_equal({"innerHtml" => [{"html" => "<span>winning</span>"}]}, @operation_builder.operations_payload)
end

test "should use previous_selector if present and should use `selector` if explicitly provided" do
@operation_builder.add_operation_method("inner_html")
@operation_builder.add_operation_method("set_focus")

@operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>").inner_html(html: "<span>I rock too</span>", selector: "#smelly2")

operations = {
"setFocus" => [
{"selector" => "#smelly"}
],
"innerHtml" => [
{"html" => "<span>I rock</span>", "selector" => "#smelly"},
{"html" => "<span>I rock too</span>", "selector" => "#smelly2"}
]
}

assert_equal(operations, @operation_builder.operations_payload)
end
end

0 comments on commit 7cea7eb

Please sign in to comment.