cable_ready 4.5.0 → 5.0.0.pre0

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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CableReady
4
- VERSION = "4.5.0"
4
+ VERSION = "5.0.0.pre0"
5
5
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CableReady::ChannelGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ class_option :stream_from, type: :string
7
+ class_option :stream_for, type: :string
8
+ class_option :stimulus, type: :boolean
9
+
10
+ def check_options
11
+ raise "Can't specify --stream-from and --stream-for at the same time" if options.key?(:stream_from) && options.key?(:stream_for)
12
+ end
13
+
14
+ def create_channel
15
+ generate "channel", file_name
16
+ end
17
+
18
+ def enhance_channels
19
+ if using_broadcast_to?
20
+ gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_for #{resource}.find(params[:id])\n"
21
+ template "app/javascript/controllers/%file_name%_controller.js" if using_stimulus?
22
+ else
23
+ prepend_to_file "app/javascript/channels/#{file_name}_channel.js", "import CableReady from 'cable_ready'\n"
24
+ inject_into_file "app/javascript/channels/#{file_name}_channel.js", after: "// Called when there's incoming data on the websocket for this channel\n" do
25
+ <<-JS
26
+ if (data.cableReady) CableReady.perform(data.operations)
27
+ JS
28
+ end
29
+
30
+ gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_from \"#{identifier}\"\n"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def option_given?
37
+ options.key?(:stream_from) || options.key?(:stream_for)
38
+ end
39
+
40
+ def using_broadcast_to?
41
+ @using_broadcast_to ||= option_given? ? options.key?(:stream_for) : yes?("Are you streaming to a resource using broadcast_to? (y/N)")
42
+ end
43
+
44
+ def using_stimulus?
45
+ @using_stimulus ||= options.fetch(:stimulus) {
46
+ yes?("Are you going to use a Stimulus controller to subscribe to this channel? (y/N)")
47
+ }
48
+ end
49
+
50
+ def resource
51
+ return @resource if @resource
52
+
53
+ stream_for = options.fetch(:stream_for) {
54
+ ask("Which resource are you streaming for?", default: class_name)
55
+ }
56
+
57
+ stream_for = file_name if stream_for == "stream_for"
58
+ @resource = stream_for.camelize
59
+ end
60
+
61
+ def identifier
62
+ return @identifier if @identifier
63
+
64
+ stream_from = options.fetch(:stream_from) {
65
+ ask("What is the stream identifier that goes into stream_from?", default: file_name)
66
+ }
67
+
68
+ stream_from = file_name if stream_from == "stream_from"
69
+ @identifier = stream_from.underscore
70
+ end
71
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module CableReady
6
+ class InitializerGenerator < Rails::Generators::Base
7
+ desc "Creates a CableReady initializer template in config/initializers"
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_initializer_file
11
+ copy_file "config/initializers/cable_ready.rb"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "fileutils"
5
+
6
+ module CableReady
7
+ class StreamFromGenerator < Rails::Generators::Base
8
+ desc "Initializes CableReady with a reference to the shared ActionCable consumer"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def copy_controller_file
12
+ main_folder = defined?(Webpacker) ? Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "") : "app/javascript"
13
+
14
+ filepath = [
15
+ "#{main_folder}/controllers/index.js",
16
+ "#{main_folder}/controllers/index.ts",
17
+ "#{main_folder}/packs/application.js",
18
+ "#{main_folder}/packs/application.ts"
19
+ ]
20
+ .select { |path| File.exist?(path) }
21
+ .map { |path| Rails.root.join(path) }
22
+ .first
23
+
24
+ lines = File.open(filepath, "r") { |f| f.readlines }
25
+
26
+ unless lines.find { |line| line.start_with?("import CableReady") }
27
+ matches = lines.select { |line| line =~ /\A(require|import)/ }
28
+ lines.insert lines.index(matches.last).to_i + 1, "import CableReady from 'cable_ready'\n"
29
+ File.open(filepath, "w") { |f| f.write lines.join }
30
+ end
31
+
32
+ unless lines.find { |line| line.start_with?("import consumer") }
33
+ matches = lines.select { |line| line =~ /\A(require|import)/ }
34
+ lines.insert lines.index(matches.last).to_i + 1, "import consumer from '../channels/consumer'\n"
35
+ File.open(filepath, "w") { |f| f.write lines.join }
36
+ end
37
+
38
+ unless lines.find { |line| line.include?("CableReady.initialize({ consumer })") }
39
+ append_to_file filepath, "CableReady.initialize({ consumer })"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ CableReady.configure do |config|
4
+ # Enable/disable exiting / warning when the sanity checks fail options:
5
+ # `:exit` or `:warn` or `:ignore`
6
+
7
+ # config.on_failed_sanity_checks = :exit
8
+
9
+ # Enable/disable exiting / warning when there's a new CableReady release
10
+ # `:exit` or `:warn` or `:ignore`
11
+
12
+ # config.on_new_version_available = :ignore
13
+
14
+ # Define your own custom operations
15
+ # https://cableready.stimulusreflex.com/customization#custom-operations
16
+
17
+ # config.add_operation_name :jazz_hands
18
+ end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cable_ready",
3
- "version": "4.4.6",
3
+ "version": "4.5.0",
4
4
  "description": "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.",
5
5
  "keywords": [
6
6
  "ruby",
@@ -15,17 +15,20 @@
15
15
  ],
16
16
  "homepage": "https://cableready.stimulusreflex.com/",
17
17
  "bugs": {
18
- "url": "https://github.com/hopsoft/cable_ready/issues"
18
+ "url": "https://github.com/stimulusreflex/cable_ready/issues"
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com:hopsoft/cable_ready.git"
22
+ "url": "git+https://github.com:stimulusreflex/cable_ready.git"
23
23
  },
24
24
  "license": "MIT",
25
25
  "author": "Nathan Hopkins <natehop@gmail.com>",
26
- "main": "./javascript/cable_ready.js",
26
+ "main": "./javascript/index.js",
27
27
  "scripts": {
28
- "prettier-standard-check": "yarn run prettier-standard --check ./javascript/**/*.js"
28
+ "lint": "yarn run prettier-standard:check",
29
+ "format": "yarn run prettier-standard:format",
30
+ "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js",
31
+ "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js"
29
32
  },
30
33
  "dependencies": {
31
34
  "morphdom": "^2.6.1"
data/tags CHANGED
@@ -11,28 +11,35 @@ CableReady lib/cable_ready/channel.rb /^module CableReady$/;" m
11
11
  CableReady lib/cable_ready/channels.rb /^module CableReady$/;" m
12
12
  CableReady lib/cable_ready/config.rb /^module CableReady$/;" m
13
13
  CableReady lib/cable_ready/version.rb /^module CableReady$/;" m
14
+ CableReady lib/generators/cable_ready/channel_generator.rb /^class CableReady::ChannelGenerator < Rails::Generators::NamedBase$/;" c
15
+ CableReady test/lib/generators/cable_ready/channel_generator_test.rb /^class CableReady::ChannelGeneratorTest < Rails::Generators::TestCase$/;" c
14
16
  Channel lib/cable_ready/channel.rb /^ class Channel$/;" c class:CableReady
15
17
  Channels lib/cable_ready/channels.rb /^ class Channels$/;" c class:CableReady
18
+ ClassMethods test/support/generator_test_helpers.rb /^ module ClassMethods$/;" m class:GeneratorTestHelpers
16
19
  Config lib/cable_ready/config.rb /^ class Config$/;" c class:CableReady
17
20
  Engine lib/cable_ready.rb /^ class Engine < Rails::Engine$/;" c class:CableReady
21
+ GeneratorTestHelpers test/support/generator_test_helpers.rb /^module GeneratorTestHelpers$/;" m
18
22
  [] lib/cable_ready/channels.rb /^ def [](identifier)$/;" f class:CableReady.Channels
19
- add_operation_definition lib/cable_ready/config.rb /^ def add_operation_definition(name)$/;" f class:CableReady.Config
20
23
  add_operation_method lib/cable_ready/channel.rb /^ def add_operation_method(name)$/;" f class:CableReady.Channel
24
+ add_operation_name lib/cable_ready/config.rb /^ def add_operation_name(name)$/;" f class:CableReady.Config
21
25
  broadcast lib/cable_ready/channel.rb /^ def broadcast(clear: true)$/;" f class:CableReady.Channel
22
26
  broadcast lib/cable_ready/channels.rb /^ def broadcast(*identifiers, clear: true)$/;" f class:CableReady.Channels
23
27
  broadcast_to lib/cable_ready/channel.rb /^ def broadcast_to(model, clear: true)$/;" f class:CableReady.Channel
24
28
  broadcast_to lib/cable_ready/channels.rb /^ def broadcast_to(model, *identifiers, clear: true)$/;" f class:CableReady.Channels
25
29
  broadcastable_operations lib/cable_ready/channel.rb /^ def broadcastable_operations$/;" f class:CableReady.Channel
26
30
  cable_ready lib/cable_ready/broadcaster.rb /^ def cable_ready$/;" f class:CableReady.Broadcaster
31
+ check_options lib/generators/cable_ready/channel_generator.rb /^ def check_options$/;" f class:CableReady
27
32
  config lib/cable_ready.rb /^ def self.config$/;" F class:CableReady
28
33
  configure lib/cable_ready.rb /^ def self.configure$/;" F class:CableReady
29
34
  const.bubbles javascript/utils.js /^ const init = { bubbles: true, cancelable: true, detail: detail }$/;" p
30
35
  const.cancelable javascript/utils.js /^ const init = { bubbles: true, cancelable: true, detail: detail }$/;" p
31
36
  const.detail javascript/utils.js /^ const init = { bubbles: true, cancelable: true, detail: detail }$/;" p
32
- const.pushState javascript/cable_ready.js /^ pushState: config => {$/;" p
33
- const.value javascript/callbacks.js /^ const ignore = { value: true }$/;" p
37
+ const.value javascript/morph_callbacks.js /^ const ignore = { value: true }$/;" p
38
+ create_channel lib/generators/cable_ready/channel_generator.rb /^ def create_channel$/;" f class:CableReady
39
+ create_sample_app test/support/generator_test_helpers.rb /^ def create_sample_app$/;" f class:GeneratorTestHelpers.ClassMethods
34
40
  default_operation_names lib/cable_ready/config.rb /^ def default_operation_names$/;" f class:CableReady.Config
35
41
  dom_id lib/cable_ready/broadcaster.rb /^ def dom_id(record, prefix = nil)$/;" f class:CableReady.Broadcaster
42
+ enhance_channels lib/generators/cable_ready/channel_generator.rb /^ def enhance_channels$/;" f class:CableReady
36
43
  export.INPUT javascript/enums.js /^ INPUT: true,$/;" p
37
44
  export.OPTION javascript/enums.js /^ OPTION: true$/;" p
38
45
  export.SELECT javascript/enums.js /^ SELECT: true$/;" p
@@ -55,8 +62,19 @@ export.textarea javascript/enums.js /^ textarea: true,$/;" p
55
62
  export.time javascript/enums.js /^ time: true,$/;" p
56
63
  export.url javascript/enums.js /^ url: true,$/;" p
57
64
  export.week javascript/enums.js /^ week: true$/;" p
65
+ finalizer_for lib/cable_ready/channel.rb /^ def self.finalizer_for(identifier)$/;" F class:CableReady.Channel
66
+ identifier lib/generators/cable_ready/channel_generator.rb /^ def identifier$/;" f class:CableReady
67
+ included test/support/generator_test_helpers.rb /^ def self.included(base)$/;" F class:GeneratorTestHelpers
58
68
  initialize lib/cable_ready/channel.rb /^ def initialize(identifier)$/;" f class:CableReady.Channel
59
69
  initialize lib/cable_ready/channels.rb /^ def initialize$/;" f class:CableReady.Channels
60
70
  initialize lib/cable_ready/config.rb /^ def initialize$/;" f class:CableReady.Config
71
+ observers lib/cable_ready/config.rb /^ def observers$/;" f class:CableReady.Config
61
72
  operation_names lib/cable_ready/config.rb /^ def operation_names$/;" f class:CableReady.Config
73
+ option_given? lib/generators/cable_ready/channel_generator.rb /^ def option_given?$/;" f class:CableReady
74
+ prepare_destination test/support/generator_test_helpers.rb /^ def prepare_destination$/;" f class:GeneratorTestHelpers.ClassMethods
75
+ remove_sample_app test/support/generator_test_helpers.rb /^ def remove_sample_app$/;" f class:GeneratorTestHelpers.ClassMethods
62
76
  reset lib/cable_ready/channel.rb /^ def reset$/;" f class:CableReady.Channel
77
+ resource lib/generators/cable_ready/channel_generator.rb /^ def resource$/;" f class:CableReady
78
+ sample_app_path test/support/generator_test_helpers.rb /^ def sample_app_path$/;" f class:GeneratorTestHelpers.ClassMethods
79
+ using_broadcast_to? lib/generators/cable_ready/channel_generator.rb /^ def using_broadcast_to?$/;" f class:CableReady
80
+ using_stimulus? lib/generators/cable_ready/channel_generator.rb /^ def using_stimulus?$/;" f class:CableReady
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "../../../lib/cable_ready"
5
+
6
+ class CableReady::CableCarTest < ActiveSupport::TestCase
7
+ setup do
8
+ @cable_car = CableReady::CableCar.instance
9
+ end
10
+
11
+ test "dispatch should return json-ifiable payload" do
12
+ CableReady::CableCar.instance.reset!
13
+ dispatch = CableReady::CableCar.instance.inner_html(selector: "#users", html: "<span>winning</span>").dispatch
14
+ assert_equal({"innerHtml" => [{"selector" => "#users", "html" => "<span>winning</span>"}]}, dispatch)
15
+ end
16
+
17
+ test "dispatch should clear operations" do
18
+ CableReady::CableCar.instance.reset!
19
+ CableReady::CableCar.instance.inner_html(selector: "#users", html: "<span>winning</span>").dispatch
20
+ assert_equal({}, CableReady::CableCar.instance.instance_variable_get(:@enqueued_operations))
21
+ end
22
+
23
+ test "dispatch should maintain operations if clear is false" do
24
+ CableReady::CableCar.instance.reset!
25
+ CableReady::CableCar.instance.inner_html(selector: "#users", html: "<span>winning</span>").dispatch(clear: false)
26
+ assert_equal({"inner_html" => [{"selector" => "#users", "html" => "<span>winning</span>"}]}, CableReady::CableCar.instance.instance_variable_get(:@enqueued_operations))
27
+ end
28
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "../../../lib/cable_ready"
5
+
6
+ class User
7
+ include ActiveModel::Model
8
+
9
+ attr_accessor :id
10
+ end
11
+
12
+ class CableReady::IdentifiableTest < ActiveSupport::TestCase
13
+ include CableReady::Identifiable
14
+
15
+ test "should handle nil" do
16
+ assert_equal "#", dom_id(nil)
17
+ end
18
+
19
+ test "should work with strings" do
20
+ assert_equal "#users", dom_id("users")
21
+ assert_equal "#users", dom_id("users ")
22
+ assert_equal "#users", dom_id(" users ")
23
+ end
24
+
25
+ test "should work with symbols" do
26
+ assert_equal "#users", dom_id(:users)
27
+ assert_equal "#active_users", dom_id(:active_users)
28
+ end
29
+
30
+ test "should just return one hash" do
31
+ assert_equal "#users", dom_id("users")
32
+ assert_equal "#users", dom_id("#users")
33
+ assert_equal "#users", dom_id("##users")
34
+ end
35
+
36
+ test "should strip prefixes" do
37
+ assert_equal "#active_users", dom_id(" users ", " active ")
38
+ assert_equal "#all_active_users", dom_id(" users ", " all_active ")
39
+ end
40
+
41
+ test "should not include provided prefix if prefix is nil" do
42
+ assert_equal "#users", dom_id("users", nil)
43
+ end
44
+
45
+ test "should work with ActiveRecord::Relation" do
46
+ relation = mock("ActiveRecord::Relation")
47
+
48
+ relation.stubs(:is_a?).with(ActiveRecord::Relation).returns(true).at_least_once
49
+ relation.stubs(:is_a?).with(ActiveRecord::Base).never
50
+ relation.stubs(:model_name).returns(OpenStruct.new(plural: "users"))
51
+
52
+ assert_equal "#users", dom_id(relation)
53
+ assert_equal "#users", dom_id(relation, nil)
54
+ assert_equal "#active_users", dom_id(relation, "active")
55
+ end
56
+
57
+ test "should work with ActiveRecord::Base" do
58
+ User.any_instance.stubs(:is_a?).with(ActiveRecord::Relation).returns(false)
59
+ User.any_instance.stubs(:is_a?).with(ActiveRecord::Base).returns(true)
60
+
61
+ assert_equal "#new_user", dom_id(User.new(id: nil))
62
+
63
+ user = User.new(id: 42)
64
+
65
+ assert_equal "#user_42", dom_id(user)
66
+ assert_equal "#user_42", dom_id(user, nil)
67
+ assert_equal "#all_active_user_42", dom_id(user, "all_active")
68
+
69
+ user = User.new(id: 99)
70
+
71
+ assert_equal "#user_99", dom_id(user)
72
+ assert_equal "#user_99", dom_id(user, nil)
73
+ assert_equal "#all_active_user_99", dom_id(user, "all_active")
74
+ end
75
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "../../../lib/cable_ready"
5
+
6
+ class CableReady::OperationBuilderTest < ActiveSupport::TestCase
7
+ setup do
8
+ @operation_builder = CableReady::OperationBuilder.new("test")
9
+ end
10
+
11
+ test "should create enqueued operations" do
12
+ assert_not_nil @operation_builder.instance_variable_get(:@enqueued_operations)
13
+ end
14
+
15
+ test "should add observer to cable ready" do
16
+ assert_not_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
17
+ end
18
+
19
+ test "should remove observer when destroyed" do
20
+ @operation_builder = nil
21
+ assert_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
22
+ end
23
+
24
+ test "should add operation method" do
25
+ @operation_builder.add_operation_method("foobar")
26
+ assert @operation_builder.respond_to?(:foobar)
27
+ end
28
+
29
+ test "added operation method should add keys" do
30
+ @operation_builder.add_operation_method("foobar")
31
+ @operation_builder.foobar({name: "passed_option"})
32
+
33
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
34
+
35
+ assert_equal 1, operations["foobar"].size
36
+ assert_equal({"name" => "passed_option"}, operations["foobar"].first)
37
+ end
38
+
39
+ test "should json-ify operations" do
40
+ @operation_builder.add_operation_method("foobar")
41
+ @operation_builder.foobar({name: "passed_option"})
42
+ assert_equal("{\"foobar\":[{\"name\":\"passed_option\"}]}", @operation_builder.to_json)
43
+ end
44
+
45
+ test "should apply! many operations" do
46
+ @operation_builder.apply!(foobar: [{name: "passed_option"}])
47
+
48
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
49
+ assert_equal 1, operations["foobar"].size
50
+ assert_equal({"name" => "passed_option"}, operations["foobar"].first)
51
+ end
52
+
53
+ test "should apply! many operations from a string" do
54
+ @operation_builder.apply!(JSON.generate({foobar: [{name: "passed_option"}]}))
55
+
56
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
57
+ assert_equal 1, operations["foobar"].size
58
+ assert_equal({"name" => "passed_option"}, operations["foobar"].first)
59
+ end
60
+
61
+ test "operations payload should omit empty operations" do
62
+ @operation_builder.add_operation_method("foobar")
63
+ payload = @operation_builder.operations_payload
64
+ assert_equal({}, payload)
65
+ end
66
+
67
+ test "operations payload should camelize keys" do
68
+ @operation_builder.add_operation_method("foo_bar")
69
+ @operation_builder.foo_bar({beep_boop: "passed_option"})
70
+ assert_equal({"fooBar" => [{"beepBoop" => "passed_option"}]}, @operation_builder.operations_payload)
71
+ end
72
+
73
+ test "should take first argument as selector" do
74
+ @operation_builder.add_operation_method("inner_html")
75
+
76
+ @operation_builder.inner_html("#smelly", html: "<span>I rock</span>")
77
+
78
+ operations = {
79
+ "innerHtml" => [{"html" => "<span>I rock</span>", "selector" => "#smelly"}]
80
+ }
81
+
82
+ assert_equal(operations, @operation_builder.operations_payload)
83
+ end
84
+
85
+ test "should use previously passed selector in next operation" do
86
+ @operation_builder.add_operation_method("inner_html")
87
+ @operation_builder.add_operation_method("set_focus")
88
+
89
+ @operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>")
90
+
91
+ operations = {
92
+ "setFocus" => [{"selector" => "#smelly"}],
93
+ "innerHtml" => [{"html" => "<span>I rock</span>", "selector" => "#smelly"}]
94
+ }
95
+
96
+ assert_equal(operations, @operation_builder.operations_payload)
97
+ end
98
+
99
+ test "should clear previous_selector after calling reset!" do
100
+ @operation_builder.add_operation_method("inner_html")
101
+ @operation_builder.inner_html(selector: "#smelly", html: "<span>I rock</span>")
102
+
103
+ @operation_builder.reset!
104
+
105
+ @operation_builder.inner_html(html: "<span>winning</span>")
106
+
107
+ assert_equal({"innerHtml" => [{"html" => "<span>winning</span>"}]}, @operation_builder.operations_payload)
108
+ end
109
+
110
+ test "should use previous_selector if present and should use `selector` if explicitly provided" do
111
+ @operation_builder.add_operation_method("inner_html")
112
+ @operation_builder.add_operation_method("set_focus")
113
+
114
+ @operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>").inner_html(html: "<span>I rock too</span>", selector: "#smelly2")
115
+
116
+ operations = {
117
+ "setFocus" => [
118
+ {"selector" => "#smelly"}
119
+ ],
120
+ "innerHtml" => [
121
+ {"html" => "<span>I rock</span>", "selector" => "#smelly"},
122
+ {"html" => "<span>I rock too</span>", "selector" => "#smelly2"}
123
+ ]
124
+ }
125
+
126
+ assert_equal(operations, @operation_builder.operations_payload)
127
+ end
128
+ end