cable_ready 4.5.0 → 5.0.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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