hotwired / turbo

The speed of a single-page web application without having to write any JavaScript
https://turbo.hotwired.dev
MIT License
6.65k stars 421 forks source link

Feature Request: declarative connection to a Turbo Stream using the `turbo-stream` custom tag #413

Closed delitescere closed 2 years ago

delitescere commented 2 years ago

Currently, some JavaScript is needed to connect a Turbo Stream to an EventSource (SSE) or WebSocket (by the way, the docs aren't very clear about how to do this).

It would be superb if it could be done without JavaScript, rather using an incantation of the turbo-stream custom element.

Current imperative mechanism:

(window['EventSource'] && window['Turbo']) ?
  Turbo.connectStreamSource(new EventSource('/my-turbo-stream')) :
  console.warn('Turbo Streams over SSE not available');

Suggested declarative mechanism:

<turbo-stream src="/my-turbo-stream">

If the URL scheme was ws: then Turbo would create a new WebSocket, otherwise it would be an EventSource.

The addition of a src attribute to the custom turbo-stream tag need not (but may) be conflated with the use of it in directives to altering the DOM as content arrives on the stream or as the result of a form submission (i.e. the existing capabilities of the custom tag).

seanpdoyle commented 2 years ago

Could you share a use case that you have in mind to help demonstrate this feature's utility?

There seems to be some confusion between concepts.

A <turbo-stream> represents a single DOM operation, and disappears once it's connected to the page.

The @hotwired/turbo-rails package's <turbo-cable-stream-source> is a persistently connected element. Its [channel] and [signed-stream-name] attributes are forwarded to the Rails application's underlying Action Cable connection. Action Cable does not yet support connecting over EventSource instances, or ActionController::Live integration. My hunch is that the channel and stream-name attributes are serving the purpose of a ws:// or wss:// web socket URL at a different layer of abstraction.

Since the <turbo-cable-stream-source> element is declared in @hotwired/turbo-rails, that repository might be a better place to open an Issue. If your use case is outside of Rails, I'm interpreting the scope of the Issue as much larger, including the introduction of a backend agnostic version of the <turbo-cable-stream-source>.

Am I misinterpreting anything here?

delitescere commented 2 years ago

Yes, this has nothing to do with Rails, the turbo-rails package, nor any server-side code. This is something entirely for @hotwired/turbo.js on the client side, just like a <turbo-frame>, &c.

In the case the <turbo-stream> custom element is present in a response entity of type text/vnd.turbo-stream.html, the treatment of the element would not change.

This proposal is for allowing the use of the same custom element in a HTML page (of type text/html) such that the turbo library would process it in a declarative manner in order to open a Turbo Stream, replacing the need for imperative, error-prone JavaScript.

The likely placement of the <turbo-stream src="..."> element when used this way would be as a child of <head> element, as that is where the imperative JavaScript incantation would currently go too.

Your "seems to be some confusion between concepts" comment indeed mirrors my "need not (but may) be conflated with" thought ;-)

seanpdoyle commented 2 years ago

Thank you for clarifying. Could you share some code or pseudo code to demonstrate the utility?

Presuming the turbo-stream were rendered in the HTML on the server side, what value is there in making an additional HTTP request to the src URL? Would rendering the element directly with contents suffice?

delitescere commented 2 years ago

I think you're still confused? This isn't about the content of a turbo-stream, this is about connecting to a turbo-stream.

Without Rails, how do you currently get a browser to connect to an EventStream (or WebSocket) that contains TurboStream content?

You add this to the page (that could be a static HTML file) that's going to receive that stream and have parts of its DOM updated by Turbo:

<head>
    <script>
        (window['EventSource'] && window['Turbo']) ?
          Turbo.connectStreamSource(new EventSource('/my-turbo-stream')) :
          console.warn('Turbo Streams over SSE not available');
    </script>
</head>
<body>
    <div id="some_target_of_my_turbo_stream"><div>
</body>

All this feature request proposes is to remove the need for the <script> tag and its contents, replacing it with something Hotwire-esque and declarative:

<head>
    <turbo-stream src="/my-turbo-stream" />
</head>
<body>
    <div id="some_target_of_my_turbo_stream"><div>
</body>

That <turbo-stream> tag is processed by @hotwire/turbo.js at the same time as <turbo-frame> elements (on DOMContentLoaded) to do the same thing as the JavaScript it would replace, along with checking the URL scheme to decide whether to open a WebSocket or an EventStream.

Does calling it <turbo-stream-connection src="/my-turbo-stream" /> make it more obvious (albeit wholly unnecessary otherwise)?

seanpdoyle commented 2 years ago

Thank you for sharing that code, I think we're aligned now.

To re-contextualize a part of my original comment:

If your use case is outside of Rails, I'm interpreting the scope of the Issue as much larger, including the introduction of a backend agnostic version of the turbo-cable-stream-source.

I think we're still discussing the details of a backend agnostic version of the turbo-cable-stream-source element. Based on your code sample, I'm interpreting that you're suggesting the current turbo-stream element be extended. Is that correct?

I think overloading the element to behave differently based on the presence or absence of a src attribute could be tricky. Maybe a new turbo-stream-source element (like the Rails version, but without the "cable") could serve that single purpose.

Is there something about extending the semantics and behavior of the existing turbo-stream element that's more compelling than introducing a new element?

seanpdoyle commented 2 years ago

I've opened https://github.com/hotwired/turbo/pull/415 to implement this. @delitescere Could you weigh in there on whether or not it fits your use case?

delitescere commented 2 years ago

I appreciate it might be tricky, indeed.

To me, it’s the same duality as “turbo-frame acting as a source” and “turbo-frame acting as a target”.

But if it were named slightly differently, that is less important than having it at all. Of course, as I’m not writing it, I defer to the developers to choose the smoothest implementation.

Much appreciated!

Also see https://link.medium.com/hcGxj0262jb for more (a lot more) context