From fa675888d3affb84341a56189611cb4cb636a828 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 8 Jan 2023 20:00:38 +0100 Subject: [PATCH 1/6] Derive stream actions arguments from context First, we add a `turbo` helper in Rails views, so we can do: ```ruby Turbo::Elements::TurboStream.new(action: "morph", target: "post_1", view_context: self, partial: "posts/post", locals: { post: @post }) turbo.stream action: "morph", target: "post_1", partial: "posts/post", locals: { post: @post } ``` Next, it's now possible to register custom stream actions, which are just shorthands: ```ruby Turbo::Ruby.stream_actions do register :morph # Shorthand to define a method `morph` that calls `stream` behind the scenes. end turbo.morph target: "post_1", partial: "posts/post", locals: { post: @post } ``` There's also specific target contexts to help build stream actions. ```ruby turbo.for(@posts).morph partial: "posts/post" # Will generate a `morph` action for each post. turbo.target(@post) do |target| target.frame do target.stream action: "yolo" end end ``` Note: we'll pass along `object:` when `**rendering`, so users can spare passing `locals:`: ```ruby turbo.morph @post, partial: "posts/post" ``` There's also specific `frame` methods: ```ruby turbo.frame @post turbo.target(@post).frame turbo.(@posts).frame do |post| # Generate a frame for each post, yielding it into the block. end ``` --- Gemfile.lock | 1 + README.md | 4 +- lib/turbo/elements/turbo_stream.rb | 2 +- lib/turbo/ruby.rb | 28 +++++++++ lib/turbo/ruby/context.rb | 83 +++++++++++++++++++++++++++ lib/turbo/ruby/railtie.rb | 13 +++++ lib/turbo/ruby/view_context_helper.rb | 11 ++++ 7 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 lib/turbo/ruby/context.rb create mode 100644 lib/turbo/ruby/railtie.rb create mode 100644 lib/turbo/ruby/view_context_helper.rb diff --git a/Gemfile.lock b/Gemfile.lock index 96504a4..efeb7be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,6 +36,7 @@ GEM zeitwerk (2.6.6) PLATFORMS + arm64-darwin-20 x86_64-darwin-19 x86_64-linux diff --git a/README.md b/README.md index c8f42c0..80b59e2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ If bundler is not being used to manage dependencies, install the gem by executin ## Note: Usage in Rails -In order to use `turbo-ruby` in Rails with the Rails `render` method you have to install the `phlex-rails` gem in your app. +In order to use `turbo-ruby` in Rails with the Rails `render` method you have to install the `phlex-rails` gem in your app. ### Regular Element @@ -52,7 +52,7 @@ end.to_html ```html+erb -<%= render Turbo::Elements::TurboStream.new(action: "morph", target: "post_1", view_context: self, partial: "posts/post", locals: { post: @post } %> +<%= render turbo.morph(@post, partial: "posts/post") %> ``` ## Development diff --git a/lib/turbo/elements/turbo_stream.rb b/lib/turbo/elements/turbo_stream.rb index 16b1d83..955af8b 100644 --- a/lib/turbo/elements/turbo_stream.rb +++ b/lib/turbo/elements/turbo_stream.rb @@ -39,7 +39,7 @@ def render_template(&block) @view_context.capture(&block) elsif @rendering.any? throw "no view_context error" if @view_context.nil? - @view_context.render(formats: [:html], **@rendering) + @view_context.render(formats: [:html], object: @target, **@rendering) elsif @allow_inferred_rendering render_record(@target) end diff --git a/lib/turbo/ruby.rb b/lib/turbo/ruby.rb index 238d736..b792f13 100644 --- a/lib/turbo/ruby.rb +++ b/lib/turbo/ruby.rb @@ -10,5 +10,33 @@ module Turbo module Ruby + module RegisteredStreamActions + def self.register(name, action = name) + define_method name do |*arguments, **options, &block| + stream(*arguments, action: action, **options, &block) + end + end + + def stream(**options, &block) + Turbo::Elements::TurboStream.new(**options, &block) + end + end + + class << self + def registered_stream_actions + RegisteredStreamActions + end + + def stream_actions(&block) + registered_stream_actions.module_eval(&block) + end + end + + stream_actions do + register :morph + register :log, "console_log" + end end end + +require_relative "railtie" if defined?(Rails::Railtie) diff --git a/lib/turbo/ruby/context.rb b/lib/turbo/ruby/context.rb new file mode 100644 index 0000000..5080698 --- /dev/null +++ b/lib/turbo/ruby/context.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Turbo + module Ruby + class Context + include Turbo::Ruby.registered_stream_actions + + attr_reader :view_context + + def initialize(view_context) + @view_context = view_context + end + + # turbo.targets(@posts).morph + def targets(targets) + if block_given? + targets.each { yield Target.new(self, _1) } + else + Targets.new(self, targets) + end + end + alias for targets # turbo.for(@post).morph or turbo.for(@posts).morph + alias call targets # turbo.(@post).morph or turbo.(@posts).morph + + def target(record) + Target.new(self, record).tap { yield _1 if block_given? } + end + + # turbo.morph(@posts) or turbo.morph(@post) + def stream(records, **options, &block) + if records.respond_to?(:each) + super(targets: records, view_context: view_context, **options, &block) + else + super(target: records, view_context: view_context, **options, &block) + end + end + + # <%= turbo.frame @post do %> + # <% end %> + def frame(record) + Turbo::Elements::TurboFrame.new(to_dom_id(record)).tap { yield record if block_given? } + end + + def to_dom_id(record) + record.respond_to?(:to_key) ? @view_context.dom_id(record) : record + end + + class Targets + include Turbo::Ruby.registered_stream_actions + + def initialize(context, targets) + @context = context + @targets = targets.map { context.to_dom_id(_1) } + end + + def frame(&block) + @targets.map { @context.frame(_1, &block) } + end + + def stream(**options, &block) + super(targets: @targets, view_context: @context.view_context, **options, &block) + end + end + + class Target + include Turbo::Ruby.registered_stream_actions + + def initialize(context, target) + @context = context + @target = context.to_dom_id(target) + end + + def frame(&block) + @context.frame(@target, &block) + end + + def stream(**options, &block) + super(target: @target, view_context: @context.view_context, **options, &block) + end + end + end + end +end diff --git a/lib/turbo/ruby/railtie.rb b/lib/turbo/ruby/railtie.rb new file mode 100644 index 0000000..c777da3 --- /dev/null +++ b/lib/turbo/ruby/railtie.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Turbo + module Ruby + class Railtie < Rails::Railtie + initializer "turbo.ruby.view_helpers" do + ActiveSupport.on_load :action_view do + include Turbo::Ruby::ViewContextHelper + end + end + end + end +end diff --git a/lib/turbo/ruby/view_context_helper.rb b/lib/turbo/ruby/view_context_helper.rb new file mode 100644 index 0000000..23c95ec --- /dev/null +++ b/lib/turbo/ruby/view_context_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Turbo + module Ruby + module ViewContextHelper + def turbo + @turbo ||= Turbo::Ruby::Context.new(self) + end + end + end +end From 9d779c65e411ddc8c6ea24258ad09e4b544b0ce4 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 8 Jan 2023 20:42:40 +0100 Subject: [PATCH 2/6] Trial out including the registered actions at the top level --- README.md | 43 +++++++++++++++++++++++++++++++++++++------ lib/turbo/ruby.rb | 3 +++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 80b59e2..8ed2162 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,19 @@ In order to use `turbo-ruby` in Rails with the Rails `render` method you have to ```ruby # Ruby -Turbo::Elements::TurboStream.new(action: "console_log", message: "Hello World").to_html +Turbo.stream(action: "console_log", message: "Hello World").to_html ``` ```html+erb -<%= render Turbo::Elements::TurboStream.new(action: "console_log", message: "Hello World") %> +<%= render Turbo.stream(action: "console_log", message: "Hello World") %> ``` - - ### Blocks ```ruby # Ruby -Turbo::Elements::TurboStream.new(action: "morph", target: "post_1") do +Turbo.stream(action: "morph", target: "post_1") do %(

Post 1

) @@ -41,13 +39,46 @@ end.to_html ```html+erb -<%= render Turbo::Elements::TurboStream.new(action: "morph", target: "post_1") do %> +<%= render Turbo.stream(action: "morph", target: "post_1") do %>

Post 1

<% end %> ``` +### Registering custom stream actions + +It's also possible to register custom stream actions: + +```ruby +Turbo::Ruby.stream_actions do + # Can either register via the shorthand helper: + register :morph + + def log(message, **options, &block) + stream(action: "console_log", message: message, **options, &block) + end + + # Or define a custom action that must convert any positional arguments into the + # appropriate keyword arguments and must call `stream`. + def custom_action(*arguments, **options, &block) + stream(**options, &block) + end +end +``` + +Now the examples from above can be: + +```ruby +Turbo.log("Hello world").to_html + +Turbo.morph target: "post_1" do + %(
+

Post 1

+
) +end.to_html +``` + ### Partials (Rails only) ```html+erb diff --git a/lib/turbo/ruby.rb b/lib/turbo/ruby.rb index b792f13..319af99 100644 --- a/lib/turbo/ruby.rb +++ b/lib/turbo/ruby.rb @@ -37,6 +37,9 @@ def stream_actions(&block) register :log, "console_log" end end + + # Make `Turbo.morph` etc. and `Turbo.stream` available. + include Ruby.registered_stream_actions end require_relative "railtie" if defined?(Rails::Railtie) From 6e82e6d6edffce7fc5724a93933096c816abaeee Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 8 Jan 2023 20:43:30 +0100 Subject: [PATCH 3/6] More accurate log implementation --- lib/turbo/ruby.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/turbo/ruby.rb b/lib/turbo/ruby.rb index 319af99..06cc2bc 100644 --- a/lib/turbo/ruby.rb +++ b/lib/turbo/ruby.rb @@ -34,7 +34,10 @@ def stream_actions(&block) stream_actions do register :morph - register :log, "console_log" + + def log(message, **options, &block) + stream(action: "console_log", message: message, **options, &block) + end end end From c4c85e0173e0b61d219eef233a880923a43f4546 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 8 Jan 2023 20:53:26 +0100 Subject: [PATCH 4/6] Automatically extract a view_context from a new context object --- lib/turbo/ruby.rb | 9 +++++++-- lib/turbo/ruby/context.rb | 12 ++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/turbo/ruby.rb b/lib/turbo/ruby.rb index 06cc2bc..fea19be 100644 --- a/lib/turbo/ruby.rb +++ b/lib/turbo/ruby.rb @@ -17,9 +17,14 @@ def self.register(name, action = name) end end - def stream(**options, &block) - Turbo::Elements::TurboStream.new(**options, &block) + def stream(view_context: view_context, **options, &block) + Turbo::Elements::TurboStream.new(view_context: view_context, **options, &block) end + + def context + self + end + delegate :view_context, to: :context end class << self diff --git a/lib/turbo/ruby/context.rb b/lib/turbo/ruby/context.rb index 5080698..d8b42cd 100644 --- a/lib/turbo/ruby/context.rb +++ b/lib/turbo/ruby/context.rb @@ -29,9 +29,9 @@ def target(record) # turbo.morph(@posts) or turbo.morph(@post) def stream(records, **options, &block) if records.respond_to?(:each) - super(targets: records, view_context: view_context, **options, &block) + super(targets: records, **options, &block) else - super(target: records, view_context: view_context, **options, &block) + super(target: records, **options, &block) end end @@ -48,6 +48,8 @@ def to_dom_id(record) class Targets include Turbo::Ruby.registered_stream_actions + attr_reader :context + def initialize(context, targets) @context = context @targets = targets.map { context.to_dom_id(_1) } @@ -58,13 +60,15 @@ def frame(&block) end def stream(**options, &block) - super(targets: @targets, view_context: @context.view_context, **options, &block) + super(targets: @targets, **options, &block) end end class Target include Turbo::Ruby.registered_stream_actions + attr_reader :context + def initialize(context, target) @context = context @target = context.to_dom_id(target) @@ -75,7 +79,7 @@ def frame(&block) end def stream(**options, &block) - super(target: @target, view_context: @context.view_context, **options, &block) + super(target: @target, **options, &block) end end end From 46bc7a3d68afc12476a022a4e1db198d3feacf78 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 8 Jan 2023 21:16:41 +0100 Subject: [PATCH 5/6] Make targets/target shorthands available on every actions context --- lib/turbo/ruby.rb | 80 ++++++++++++++++++++++++++++++++++----- lib/turbo/ruby/context.rb | 69 +++------------------------------ 2 files changed, 75 insertions(+), 74 deletions(-) diff --git a/lib/turbo/ruby.rb b/lib/turbo/ruby.rb index fea19be..8b0b2e4 100644 --- a/lib/turbo/ruby.rb +++ b/lib/turbo/ruby.rb @@ -10,30 +10,50 @@ module Turbo module Ruby - module RegisteredStreamActions + module StreamActionsContext def self.register(name, action = name) define_method name do |*arguments, **options, &block| stream(*arguments, action: action, **options, &block) end end - def stream(view_context: view_context, **options, &block) - Turbo::Elements::TurboStream.new(view_context: view_context, **options, &block) + # turbo.targets(@posts).morph + def targets(targets) + if block_given? + targets.each { yield Turbo::Ruby::Target.new(self, _1) } + else + Turbo::Ruby::Targets.new(self, targets) + end + end + alias for targets # Turbo.for(@post).morph or Turbo.for(@posts).morph + alias call targets # Turbo.(@post).morph or Turbo.(@posts).morph + + def target(record) + Turbo::Ruby::Target.new(self, record).tap { yield _1 if block_given? } + end + + # <%= turbo.frame "post_1" do %> + # <% end %> + def frame(record) + Turbo::Elements::TurboFrame.new(to_dom_id(record)).tap { yield record if block_given? } + end + + def to_dom_id(record) + record end - def context - self + def stream(**options, &block) + Turbo::Elements::TurboStream.new(**options, &block) end - delegate :view_context, to: :context end class << self - def registered_stream_actions - RegisteredStreamActions + def stream_actions_context + StreamActionsContext end def stream_actions(&block) - registered_stream_actions.module_eval(&block) + stream_actions_context.module_eval(&block) end end @@ -47,7 +67,47 @@ def log(message, **options, &block) end # Make `Turbo.morph` etc. and `Turbo.stream` available. - include Ruby.registered_stream_actions + include Ruby.stream_actions_context + + class Targets + include Turbo::Ruby.stream_actions_context + + undef_method :targets + undef_method :target + + def initialize(context, targets) + @context = context + @targets = targets.map { context.to_dom_id(_1) } + end + + def frame(&block) + @targets.map { @context.frame(_1, &block) } + end + + def stream(**options, &block) + @context.stream(targets: @targets, **options, &block) + end + end + + class Target + include Turbo::Ruby.stream_actions_context + + undef_method :targets + undef_method :target + + def initialize(context, target) + @context = context + @target = context.to_dom_id(target) + end + + def frame(&block) + @context.frame(@target, &block) + end + + def stream(**options, &block) + @context.stream(target: @target, **options, &block) + end + end end require_relative "railtie" if defined?(Rails::Railtie) diff --git a/lib/turbo/ruby/context.rb b/lib/turbo/ruby/context.rb index d8b42cd..27373be 100644 --- a/lib/turbo/ruby/context.rb +++ b/lib/turbo/ruby/context.rb @@ -3,7 +3,7 @@ module Turbo module Ruby class Context - include Turbo::Ruby.registered_stream_actions + include Turbo::Ruby.stream_actions_context attr_reader :view_context @@ -11,75 +11,16 @@ def initialize(view_context) @view_context = view_context end - # turbo.targets(@posts).morph - def targets(targets) - if block_given? - targets.each { yield Target.new(self, _1) } - else - Targets.new(self, targets) - end - end - alias for targets # turbo.for(@post).morph or turbo.for(@posts).morph - alias call targets # turbo.(@post).morph or turbo.(@posts).morph - - def target(record) - Target.new(self, record).tap { yield _1 if block_given? } + def to_dom_id(record) + record.respond_to?(:to_key) ? view_context.dom_id(record) : record end # turbo.morph(@posts) or turbo.morph(@post) def stream(records, **options, &block) if records.respond_to?(:each) - super(targets: records, **options, &block) + super(targets: records, view_context: view_context, **options, &block) else - super(target: records, **options, &block) - end - end - - # <%= turbo.frame @post do %> - # <% end %> - def frame(record) - Turbo::Elements::TurboFrame.new(to_dom_id(record)).tap { yield record if block_given? } - end - - def to_dom_id(record) - record.respond_to?(:to_key) ? @view_context.dom_id(record) : record - end - - class Targets - include Turbo::Ruby.registered_stream_actions - - attr_reader :context - - def initialize(context, targets) - @context = context - @targets = targets.map { context.to_dom_id(_1) } - end - - def frame(&block) - @targets.map { @context.frame(_1, &block) } - end - - def stream(**options, &block) - super(targets: @targets, **options, &block) - end - end - - class Target - include Turbo::Ruby.registered_stream_actions - - attr_reader :context - - def initialize(context, target) - @context = context - @target = context.to_dom_id(target) - end - - def frame(&block) - @context.frame(@target, &block) - end - - def stream(**options, &block) - super(target: @target, **options, &block) + super(target: records, view_context: view_context, **options, &block) end end end From 91388fb84451dcee88ed9f291238255f606261bf Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 10 Jan 2023 12:14:37 +0100 Subject: [PATCH 6/6] Fix railtie require path --- lib/turbo/ruby.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/turbo/ruby.rb b/lib/turbo/ruby.rb index 8b0b2e4..6ed4abc 100644 --- a/lib/turbo/ruby.rb +++ b/lib/turbo/ruby.rb @@ -110,4 +110,4 @@ def stream(**options, &block) end end -require_relative "railtie" if defined?(Rails::Railtie) +require_relative "ruby/railtie" if defined?(Rails::Railtie)