Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CableReady Operation Mode #219

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ source "https://rubygems.org"

# Specify your gem's dependencies in cable_ready.gemspec
gemspec

gem 'turbo-rails', github: 'marcoroth/turbo-rails', branch: 'turbo-stream-additional-attributes'
12 changes: 12 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
GIT
remote: https://github.com/marcoroth/turbo-rails.git
revision: d38861125d86fb6b4aa70d4db44afd81e609fa03
branch: turbo-stream-additional-attributes
specs:
turbo-rails (1.1.1)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)

PATH
remote: .
specs:
Expand All @@ -9,6 +19,7 @@ PATH
activesupport (>= 5.2)
railties (>= 5.2)
thread-local (>= 1.1.0)
turbo-rails

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -193,6 +204,7 @@ DEPENDENCIES
rake
sqlite3
standardrb
turbo-rails!

BUNDLED WITH
2.2.33
1 change: 1 addition & 0 deletions cable_ready.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Gem::Specification.new do |gem|
gem.add_dependency "activerecord", rails_version
gem.add_dependency "activesupport", rails_version
gem.add_dependency "railties", rails_version
gem.add_dependency "turbo-rails"

gem.add_dependency "thread-local", ">= 1.1.0"

Expand Down
13 changes: 13 additions & 0 deletions lib/cable_ready.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@
require "cable_ready/cable_car"
require "cable_ready/stream_identifier"

require "turbo-rails"
turbo = Gem::Specification.find_by_name("turbo-rails").gem_dir

module Turbo
module Streams
end
end

require "#{turbo}/app/helpers/turbo/streams/action_helper"
require "#{turbo}/app/models/turbo/streams/tag_builder"
require "#{turbo}/app/helpers/turbo/streams_helper"


module CableReady
class << self
def config
Expand Down
3 changes: 2 additions & 1 deletion lib/cable_ready/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ class Config
include Observable
include Singleton

attr_accessor :on_failed_sanity_checks, :on_new_version_available
attr_accessor :on_failed_sanity_checks, :on_new_version_available, :operation_mode
attr_writer :verifier_key

def initialize
super
@operation_names = Set.new(default_operation_names)
@on_failed_sanity_checks = :exit
@on_new_version_available = :ignore
@operation_mode = :cable_ready
end

def observers
Expand Down
48 changes: 47 additions & 1 deletion lib/cable_ready/operation_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module CableReady
class OperationBuilder
include Identifiable
include Turbo::Streams::ActionHelper

attr_reader :identifier, :previous_selector

def self.finalizer_for(identifier)
Expand Down Expand Up @@ -58,6 +60,14 @@ def to_json(*args)
@enqueued_operations.to_json(*args)
end

def to_turbo_stream
operations_payload.join
end

def to_html
to_turbo_stream
end

def apply!(operations = "[]")
operations = begin
JSON.parse(operations.is_a?(String) ? operations : operations.to_json)
Expand All @@ -69,7 +79,43 @@ def apply!(operations = "[]")
end

def operations_payload
@enqueued_operations.map { |operation| operation.deep_transform_keys! { |key| key.to_s.camelize(:lower) } }
if ::CableReady.config.operation_mode == :turbo_stream
def translate_operation_name(name)
case name
when "innerHtml" then "replace"
else name
end
end

def single_selector?(selector)
selector.starts_with?("#")
end

def translate_selector(operation)
dom_id = operation["domId"] || operation[:dom_id] || operation["dom_id"]
return [dom_id, :target] if dom_id.present?

selector = operation["selector"]
return ["body", :targets] if selector.nil? || selector.empty?

if single_selector?(selector)
return [selector.from(1), :target]
end

[selector, :targets]
end

@enqueued_operations.map do |operation|
turbo_action = translate_operation_name(operation["operation"])
turbo_target, target_attribute = translate_selector(operation)
turbo_template = operation["html"] || operation[:html]
attributes = operation.except("operation", "selector", "html", "domId", "dom_id", :dom_id, :html).deep_transform_keys { |key| key.to_s.dasherize }

turbo_stream_action_tag(turbo_action, target_attribute => turbo_target, template: turbo_template, **attributes)
end
else
@enqueued_operations.map { |operation| operation.deep_transform_keys! { |key| key.to_s.camelize(:lower) } }
end
end

def operations_in_custom_element
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@

# config.on_new_version_available = :ignore

# Specify operations payload output format, options:
# `:cable_ready` or `:turbo_stream`

# config.operation_mode = :turbo_stream

# Define your own custom operations
# https://cableready.stimulusreflex.com/customization#custom-operations

Expand Down
186 changes: 186 additions & 0 deletions test/lib/cable_ready/operation_builder_turbo_stream_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# frozen_string_literal: true

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

class Death
def to_html
"I rock"
end

def to_dom_id
"death"
end

def to_operation_options
[:html, :dom_id, :spaz]
end
end

class Life
def to_operation_options
{
html: "You go, girl",
dom_id: "life"
}
end
end

class CableReady::OperationBuilderTurboStreamTest < ActiveSupport::TestCase
setup do
CableReady.config.operation_mode = :turbo_stream
@operation_builder = CableReady::OperationBuilder.new("test")
end

teardown do
CableReady.config.operation_mode = :cable_ready
end

test "should create enqueued operations" do
assert_not_nil @operation_builder.instance_variable_get(:@enqueued_operations)
end

test "should add observer to cable ready" do
assert_not_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
end

test "should remove observer when destroyed" do
@operation_builder = nil
assert_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
end

test "should add operation method" do
@operation_builder.add_operation_method("foobar")
assert @operation_builder.respond_to?(:foobar)
end

test "should operations convert operations to Turbo Stream / HTML" do
@operation_builder.add_operation_method("foobar")
@operation_builder.foobar({name: "passed_option"})

assert_equal("<turbo-stream name=\"passed_option\" action=\"foobar\" targets=\"body\"><template></template></turbo-stream>", @operation_builder.to_turbo_stream)
assert_equal("<turbo-stream name=\"passed_option\" action=\"foobar\" targets=\"body\"><template></template></turbo-stream>", @operation_builder.to_html)
end

test "operations payload should omit empty operations" do
@operation_builder.add_operation_method("foobar")
assert_equal([], @operation_builder.operations_payload)
assert_equal("", @operation_builder.to_html)
assert_equal("", @operation_builder.to_turbo_stream)
end

test "operations payload should camelize keys" do
@operation_builder.add_operation_method("foo_bar")
@operation_builder.foo_bar({beep_boop: "passed_option"})

operations = ["<turbo-stream beep-boop=\"passed_option\" action=\"fooBar\" targets=\"body\"><template></template></turbo-stream>"]

assert_equal(operations, @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 = ["<turbo-stream action=\"replace\" target=\"smelly\"><template><span>I rock</span></template></turbo-stream>"]

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 = [
"<turbo-stream action=\"setFocus\" target=\"smelly\"><template></template></turbo-stream>",
"<turbo-stream action=\"replace\" target=\"smelly\"><template><span>I rock</span></template></turbo-stream>"
]

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>")

operations = ["<turbo-stream action=\"replace\" targets=\"body\"><template><span>winning</span></template></turbo-stream>"]

assert_equal(operations, @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 = [
"<turbo-stream action=\"setFocus\" target=\"smelly\"><template></template></turbo-stream>",
"<turbo-stream action=\"replace\" target=\"smelly\"><template><span>I rock</span></template></turbo-stream>",
"<turbo-stream action=\"replace\" target=\"smelly2\"><template><span>I rock too</span></template></turbo-stream>",
]

assert_equal(operations, @operation_builder.operations_payload)
end

test "should pull html option from Death object" do
@operation_builder.add_operation_method("inner_html")
death = Death.new

@operation_builder.inner_html(html: death)

operations = ["<turbo-stream action=\"replace\" targets=\"body\"><template>I rock</template></turbo-stream>"]

assert_equal(operations, @operation_builder.operations_payload)
end

test "should pull html option with selector from Death object" do
@operation_builder.add_operation_method("inner_html")
death = Death.new

@operation_builder.inner_html(death, html: death)

operations = ["<turbo-stream action=\"replace\" target=\"death\"><template>I rock</template></turbo-stream>"]

assert_equal(operations, @operation_builder.operations_payload)
end

test "should pull html and dom_id options from Death object" do
@operation_builder.add_operation_method("inner_html")
death = Death.new

@operation_builder.inner_html(death)

operations = ["<turbo-stream action=\"replace\" target=\"death\"><template>I rock</template></turbo-stream>"]

assert_equal(operations, @operation_builder.operations_payload)
end

test "should pull html and dom_id options from Life object" do
@operation_builder.add_operation_method("inner_html")
life = Life.new

@operation_builder.inner_html(life)

operations = ["<turbo-stream action=\"replace\" target=\"life\"><template>You go, girl</template></turbo-stream>"]

assert_equal(operations, @operation_builder.operations_payload)
end

test "should put operation[message] into the tempalte tag" do
@operation_builder.add_operation_method("console_log")

@operation_builder.console_log(message: "Hello Console", level: "warn")

operations = ["<turbo-stream message=\"Hello Console\" level=\"warn\" action=\"consoleLog\" targets=\"body\"><template></template></turbo-stream>"]

assert_equal(operations, @operation_builder.operations_payload)
end
end