Hotwired ASP.NET Core Web Application (6 Part Series)
Hotwired ASP.NET Core Web Application — Part 1 (Intro)
Hotwired ASP.NET Core Web Application — Part 2 (NPM, Webpack Setup)
Hotwired ASP.NET Core Web Application — Part 3 (Turbo Drive & Frame)
Hotwired ASP.NET Core Web Application — Part 4 (Turbo Stream)
Hotwired ASP.NET Core Web Application — Part 5 (Stimulus)
Hotwired ASP.NET Core Web Application — Part 6 (Quote Editor)
In part 3, we have used turbo frames to replace the content of a turbo frame or to replace the whole page. But what if we needed more fine-tuning on what to do with the DOM elements such as removing or appending them instead of just replace? This is where turbo streams come into the picture.
Turbo Stream
To see why we need turbo streams and understand how they work, let’s create a simple notes list and implement its CRUD actions. As in the implementation of our previous messages, I didn’t want to use any permanent storage for the notes, not to lose our focus on Hotwire technologies. Here is the simple Note
class and singleton Notes
class which is populated with two notes initially.
Our new notes list is rendered having three different turbo frame types:
notes_0
turbo frame is a placeholder to show the_NotesAddEdit.cshtml
partial view when the Add button is clicked which is therefore initially empty.notes
turbo frame contains each note which will be used to append/prepend our newly created notes.note_{id}
turbo frames are there to work on each note independently.
Let’s start with the “create” action. When we click the Add link which targets the note_0
turbo frame, the OnGetAddNote
method is called. In this method, we create a new empty Note
instance and return the _NoteAddEdit
partial view containing an input to change the note Name
, also a Cancel link, and a Save button inside a form.
As we have seen many times, while working on messages, the response replaces the initially empty notes_0
turbo frame with the contents of the response which also contains a turbo frame with the same id
. So, far we didn’t do anything new. But now, when we want to save our new note, we want to do two things:
- Hide the new note editor at the top
- Add our new note to our
notes
turbo frame
Hiding the new note editor can be accomplished by returning an empty turbo frame with id note_0
. But how can we add the new note without returning all the notes? And this is where the turbo stream comes to the rescue. When we click the Save button, the OnPostSaveNote
method will be called which will create the new note and at the end return the _NoteAdd
partial view, with the new note being the model of this view.
Turbo Streams deliver page changes as fragments of HTML wrapped in self-executing
<turbo-stream>
elements.
Each <turbo-stream>
element has an action
and target
attribute that specifies what should happen to the HTML inside the turbo stream.
In the above, _NoteAdd
partial view contains two turbo-stream
elements:
- The first
turbo-stream
element has thetarget
attribute set tonote_0
and theaction
attribute set toreplace
. This means, it willreplace
the DOM element with idnote_0
, with the empty content to hide the editor. - And the second one has the
action="append" target="notes"
attributes that instruct to append the content inside the turbo stream to the DOM element with idnotes
.
As we see in the code, turbo-stream elements must wrap their content inside a <template>
element and also we can render more than one <turbo-stream>
element inside a single response. And the targeted element does not have to be a turbo frame; it can be any HTML element containing the corresponding id
attribute.
What about this line that we set before returning the partial view?Response.ContentType = "text/vnd.turbo-stream.html";
When a form with a POST
, PUT
, PATCH
, DELETE
method is submitted, Turbo automatically injects text/vnd.turbo-stream.html
into the set of response formats in the request’s accept header. So, Turbo is signaling that it can accept responses of this MIME type. If we click a link, for example, we will not see this MIME type in the request’s accept header.
When Turbo is ready for this content type, we also need to set this MIME type on our response. Because only Turbo applies the turbo stream updates instead of the normal merging logic, if the content type for the response is text/vnd.turbo-stream.html
. You can comment out the above line and try adding a new note. You will see that it will not work.
Now that we have covered how turbo streams work, let’s move on to the “Edit” action which is almost the same as the “Add”. When we edit a note, we call the OnGetEditNote
action, which finds the Note
instance with the given id and again returns _NotesAddEdit
partial view with the found Note
as the model of the view. And to save the update, we again call OnPostSaveNote
, which updates the Note instance and returns the _NoteEdit
partial view.
This partial view also returns a turbo stream response to replace the edited Note instance with its view (_NotesView
).
And finally, the “Delete” action calls the OnPostDeleteNote
method, which removes the Note instance from the list and returns the _NoteDelete.cshtml
partial view.
And _NoteDelete
view contains one turbo stream element that targets the deleted element (note_{deletedId}
) and action remove
which basically removes that element from the page.
Before moving on to the next section, you can download and run the code covering this part from here.
Delivering Turbo Streams over WebSockets
Turbo stream elements can also be delivered by the server over a WebSocket to bring real-time updates made by other users or processes. And to implement this in our project, we need to add some external code.
First of all, we need to render our partial views to a string to deliver them through WebSockets. For this, I have used the RazorPartialToStringRenderer
service that I have found on the “Learn Razor Pages” website. I won’t get into the details of the code, but it basically locates the given partial view, renders its output to a StringWriter
, and returns the content of the writer as a string.
Secondly, we need to add SignalR to our project. SignalR uses the WebSocket transport if available and falls back to older transports when necessary. We first create an AppHub
class that inherits from the SignalR.Hub
class, that will handle client-server communication. In our case, we only want to push our server-side HTML to clients, so we do not need to add any methods.
Now, we need to register the RazorPartialToStringRenderer
service and configure the SignalR server in our Program.cs
file.
Finally, I used scottiemc7’s signalRSourceElement.ts
code in hotwire-aspnet-demo-chat project to create the signalr-turbo-stream
HTML element that will listen to the AppHub
for turbo stream responses. I have renamed it and saved it under the ClientApp\js
folder with the name signalRTurboStreamElement.js
.
For this element to work, we also need to install the SignalR package from the terminal while being inside the ClientApp
folder:
npm install @microsoft/signalr --save
And we also import this file into our app.js
file:
import './signalRTurboStreamElement'
And now, it’s time to use these new services to deliver our turbo stream responses over the WebSocket. We will first change the constructor of IndexModel
for the injection of the two new services.
And let’s use these services inside the OnPostSaveNote
and OnPostDeleteNote
methods, where we were returning turbo stream results:
In the above code, we have commented out the parts that set the ContentType
and return PartialViewResults
. Instead, we used our RazorPartialToStringRenderer
service to render our partial views to a string, renderedViewStr
. Then we used the SignalR hub service to deliver this string to all clients with the “NotesChanged” method.
Lastly, we need to add our custom HTML element to the Index.cshtml
to connect to our AppHub and listen for the “NoteChanged” messages.
And that’s it. Now we can first check if our CRUD actions are working as before. Then, we can also test the real-time updates delivered to each client by opening two browser windows and adding, editing, and deleting notes which I have shown in the following video.
Summary
We have covered and used the three main components of Turbo:
1. Turbo Drive
2. Turbo Frame
3. Turbo Stream
However, when compared to the Rails applications, in which Turbo and Stimulus are integrated and are the default front-end framework, it is easier and more robust than our implementation in an ASP.NET application. For example, when using turbo frames, it is very important to name them with good conventions. Unless we pay attention to this, as the application grows, we might find ourselves in a naming hell. In Rails, there is a dom_id
helper which converts an object into a unique id. We can and should implement the same helper in our application which will name the turbo frames with the agreed-upon naming conventions in one place.
Also, while using turbo streams, either delivering them over HTTP or WebSockets, we can refactor our response implementations into a class to make our application more resilient against the possible changes in the Turbo library.
And the other downside of our implementation is that it is tightly coupled to turbo streams: however, as advised in turbo streams documentation, we should build a “progressively enhanced” version of our pages. But we have used partial views in all our responses instead of full-page responses.
In part 5, I will explain the Stimulus and show how we can use it in our application. You can download the code covering this part from here.
References
[1] Rendering A Partial View To A String
[2] Introduction to SignalR
[3] Tutorial: Get started with ASP.NET Core SignalR