Skip to content

Commit

Permalink
Ruby SDK support rails render_in interface (starfederation#617)
Browse files Browse the repository at this point in the history
* Support Rails' #render_in(view_context) interface

So render arbitrary objects, including component libs like ViewComponent and Phlex Rails.

* Render anything that supports #to_s, including Rails' safe buffers

* Make sure Rails view_context is an ActionView::Base

* Document
  • Loading branch information
ismasan authored and Joshswooft committed Feb 7, 2025
1 parent c3cf6cb commit 75bd8df
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 10 deletions.
51 changes: 43 additions & 8 deletions sdk/ruby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ In your Rack handler or Rails controller:

```ruby
# Rails controllers, as well as Sinatra and others,
# already have request and response objects
# already have request and response objects.
# `view_context` is optional and is used to render Rails templates.
# Or view components that need access to helpers, routes, or any other context.

datastar = Datastar.new(request:, response:, view_context: self)
datastar = Datastar.new(request:, response:, view_context:)

# In a Rack handler, you can instantiate from the Rack env
datastar = Datastar.from_rack_env(env)
Expand Down Expand Up @@ -130,7 +132,7 @@ See https://data-star.dev/reference/sse_events#datastar-execute-script

```ruby
sse.execute_scriprt(%(alert('Hello World!'))
```
```

#### `signals`
See https://data-star.dev/guide/getting_started#data-signals
Expand All @@ -139,14 +141,14 @@ Returns signals sent by the browser.

```ruby
sse.signals # => { user: { name: 'John' } }
```
```

#### `redirect`
This is just a helper to send a script to update the browser's location.

```ruby
sse.redirect('/new_location')
```
```

### Lifecycle callbacks

Expand Down Expand Up @@ -198,11 +200,14 @@ Datastar.configure do |config|
end
```

### Rails
### Rendering Rails templates

#### Rendering Rails templates
In Rails, make sure to initialize Datastar with the `view_context` in a controller.
This is so that rendered templates, components or views have access to helpers, routes, etc.

```ruby
datastar = Datastar.new(request:, response:, view_context:)

datastar.stream do |sse|
10.times do |i|
sleep 1
Expand All @@ -212,14 +217,44 @@ datastar.stream do |sse|
end
```

#### Rendering Phlex components
### Rendering Phlex components

`#merge_fragments` supports [Phlex](https://www.phlex.fun) component instances.

```ruby
sse.merge_fragments(UserComponent.new(user: User.first))
```

### Rendering ViewComponent instances

`#merge_fragments` also works with [ViewComponent](https://viewcomponent.org) instances.

```ruby
sse.merge_fragments(UserViewComponent.new(user: User.first))
```

### Rendering `#render_in(view_context)` interfaces

Any object that supports the `#render_in(view_context) => String` API can be used as a fragment.

```ruby
class MyComponent
def initialize(name)
@name = name
end

def render_in(view_context)
"<div>Hello #{@name}</div>""
end
end
```
```ruby
sse.merge_fragments MyComponent.new('Joe')
```
### Tests
```ruby
Expand Down
7 changes: 6 additions & 1 deletion sdk/ruby/lib/datastar/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
module Datastar
class Railtie < ::Rails::Railtie
FINALIZE = proc do |view_context, response|
view_context.response = response
case view_context
when ActionView::Base
view_context.controller.response = response
else
raise ArgumentError, 'view_context must be an ActionView::Base'
end
end

initializer 'datastar' do |_app|
Expand Down
10 changes: 9 additions & 1 deletion sdk/ruby/lib/datastar/server_sent_event_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,15 @@ def initialize(stream, signals:, view_context: nil)

def merge_fragments(fragments, options = BLANK_OPTIONS)
# Support Phlex components
fragments = fragments.call(view_context:) if fragments.respond_to?(:call)
# And Rails' #render_in interface
fragments = if fragments.respond_to?(:render_in)
fragments.render_in(view_context)
elsif fragments.respond_to?(:call)
fragments.call(view_context:)
else
fragments.to_s
end

fragment_lines = fragments.to_s.split("\n")

buffer = +"event: datastar-merge-fragments\n"
Expand Down
16 changes: 16 additions & 0 deletions sdk/ruby/spec/dispatcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,22 @@ def self.call(view_context:) = %(<div id="foo">\n<span>#{view_context}</span>\n<
dispatcher.response.body.call(socket)
expect(socket.lines).to eq([%(event: datastar-merge-fragments\nid: 72\nretry: 2000\ndata: settleDuration 1000\ndata: fragments <div id="foo">\ndata: fragments <span>#{view_context}</span>\ndata: fragments </div>\n\n\n)])
end

it 'works with #render_in(view_context, &) interfaces' do
template_class = Class.new do
def self.render_in(view_context) = %(<div id="foo">\n<span>#{view_context}</span>\n</div>\n)
end

dispatcher.merge_fragments(
template_class,
id: 72,
retry_duration: 2000,
settle_duration: 1000
)
socket = TestSocket.new
dispatcher.response.body.call(socket)
expect(socket.lines).to eq([%(event: datastar-merge-fragments\nid: 72\nretry: 2000\ndata: settleDuration 1000\ndata: fragments <div id="foo">\ndata: fragments <span>#{view_context}</span>\ndata: fragments </div>\n\n\n)])
end
end

describe '#remove_fragments' do
Expand Down

0 comments on commit 75bd8df

Please sign in to comment.