Hotwired ASP.NET Core Web Application — Part 3

ipek
9 min readJun 18, 2022

--

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 part2, we did set up our development environment with webpack. Now in the next three parts, we will learn Turbo and Stimulus. If you want to follow along with the tutorial, you can download the current version from here.

First, we will install Turbo from npm. Open a terminal window, go to the ClientApp directory and run the following command:

Installing Turbo as a runtime dependency

Now, replace theClientApp\src\js\app.js file content with the following:

Initializing Turbo

In the above code, we simply import Turbo and hook to its 'turbo:load' and 'turbo:visit' event handlers, just to see if it’s working. Turbo.session.drive is true by default; so we don’t need to initialize it. But we will change it to see the effect of Turbo Drive.

Now, open up two terminals and run dotnet watch run in one of them (in the project root directory) and run npm run dev in the other (in the ClientApp directory). When the Index page opens in the browser, open the developer tools and check the “Console” tab: you should see the turbo:load log entry.

Index page

Now, switch to the “Network” tab, clear the entries, and click the “Go to the Quote Editor” button.

The network tab while Turbo.session.drive = true

You should see similar requests as in the above image:
1. an HTML request to load the /Tutorial/Quotes page
2. a request to load our bundled app.js file
3. a request to load our bundled app.cs file
4. a request to load our logo image

Now, set the Turbo.session.drive to true in app.js file, go back to the Index page, clear the “Network” tab and again click the “Go to the Quote Editor” button.

The network tab while Turbo.session.drive = false

As the image shows, we now have:
1. an AJAX request to get the HTML for the Tutorial/Quotes page
2. and a request to load our logo image

You might also notice that when Turbo.session.drive is set to true, the switch to the Tutorial/Quotes page is more seamless.

So, just by importing Turbo into our application, we gained access to its Turbo Drive feature. But how does this work? What’s happening in the background?

1. Turbo Drive

Turbo Drive mechanism

Basically, Turbo Drive:

  1. Hijacks the click events on <a href> links and submit events on forms.
  2. If available, it renders a preview of the requested page (which improves the speed of navigation between the same pages).
  3. Prevents the browser from following these requests.
  4. Changes the browser’s URL using the History API.
  5. Requests the new page using fetch (making AJAX request).
  6. Renders the HTML response by replacing the current <body> element and merging the contents of the <head> element.

And in most cases, the <head> section of the page won't change, and this will result in a significant performance improvement because the requests to download the JavaScript and CSS files will only be called once when they are first accessed.

On some links or forms, we might want to disable Turbo Drive; in that case, we need to add the data-turbo="false" data attribute on the element.

On its own, Turbo Drive is great. But there are also other techniques in Turbo to create fast and progressively enhanced web applications.

2. Turbo Frames

With Turbo Frames, we can divide a page into independent pieces which can be updated automatically. Again the links and form submissions are captured, and no matter what the response is (either full or partial page), ONLY the specific <turbo-frame> parts are updated. A <turbo-frame>is identified by its id, so it must be unique.

Index.cshtml page

We have changed our Index.cshtml file as above and added a turbo-frame element with id “first-frame”, which contains a message in a p element and a link to the new MessageEdit.cshtml page file.

Index page

And here is the MessageEdit.cshtml page content. It is almost the same as the Index.cshtml page. It also has a turbo-frame element with the same “first-frame” id. But now inside this element, there is a form tag with an input element instead of the p tag and a button element instead of the a tag. And we also commented out the “Go to the Quote Editor” button.

MessageEdit.cshtml page

When we click the “Edit Me” link, Turbo again intercepts the request and turns it into a fetch, and the page instantaneously changes to show the inline editor for the message and nothing else changes.

Requesting MessageEdit.cshtml page

When the link to edit the message is clicked, the response provided by MessageEdit.cshtml has its <turbo-frame id="first-frame"> segment extracted, and the content replaces the frame with the same id from where the request originated. Also, if we check out the Request Headers on the network tab, we will see that it includes a turbo-frame entry with the id of the targeted turbo frame, which was first-frame.

Remember that we have also commented out the “Go to the Quote Editor” button in the MessageEdit.cshtml page, but it’s still there. As we have mentioned before, that’s because, ONLY content inside a matching <turbo-frame> is used when the frame is updated, the rest is ignored. If so, then we don’t need to return a full page to show our inline editor, we can just return a partial view with the updated version of the requested frame.

So, let’s add another <turbo-frame> element to our Index.cshtml page, with the id “second-frame”, to achieve the same functionality using partial views.

Index.cshtml page with the second turbo frame

When we click the link inside the <turbo-frame id="second-frame">, this time we make a request to the Index.cshtml page using a named handler method OnGetEdit, by setting asp-page="/Index" asp-page-handler="Edit" on the link.

Index.cshtml.cs

And the OnGetEdit method will return the below partial view (_MessageEdit2.cshtml) which only contains the updated <turbo-frame id="second-frame">.

_MessageEdit.cshtml partial view
Using a partial view to update the turbo frame
Partial view response with only turbo frame

And we again see our inline message editor, with just returning the partial view containing the <turbo-frame id="second-frame">. Turbo frame helped us to decompose our view into predefined parts which can be automatically updated after receiving a response.

To complete our second inline message editor, this time we will use the onPostEdit named handler when the “Save” button is clicked, to return another partial view, _MessageView2.cshtml. This partial view contains the same initial view with a p and link tags inside the <turbo-frame id="second-frame"> tag.

_MessageView.cshtml partial view

Since this is the same view for <turbo-frame id="second-frame"> part that we used in Index.cshtml, let’s avoid code duplication and also use this partial view on that page. Here is the updated Index.cshtml file using _MessageView2.cshtml partial view.

Index.cshtml page using _MessageView2 partial view
Navigating frames

In summary, if we click a link or submit a form inside a Turbo Frame,

  • If the response contains a turbo frame with the same id, Turbo will ONLY replace the content of the source turbo frame with that frame’s content.
  • If the response does not contain a turbo frame with the same id, then our turbo frame will disappear and we will also see an error in the console.

We have witnessed the first mechanism, let’s also test the error case. For that, just remove the turbo-frame tag from the _MessageEdit2.cshtml partial view and click “Edit Me”. As in the below picture, our second message frame will disappear, and we will see the “Response has no matching <turbo-frame id="second-frame"> element” error in the console.

The error when the response has no matching turbo frame

OK, what if we want the link that we click or the form that we submit to target a different turbo frame than the one they are inside, or they are not inside a turbo frame at all? Then, we use the data-turbo-frame attribute on the link and form tags, to indicate the turbo frame that we want to target. To explain this use case, we have added the “Add Message 3” button which is not inside a turbo frame. We also added two partial views inside the third-frame turbo-frame to either show the Message3 content (_MessageView3) or its inline editor (_MessageEdit3).

_MessageView3.cshtml partial view that shows the Message3
_MessageView2.cshtml partial view that edits the Message3
When the “Add Message 3” link is clicked, it reveals the inline editor by requesting _MessageEdit2 partial view
Index.cshtml page content with the new “Add Message 3” link and _MessageView3 partial view added

In the above code, you can see the newly added “Add Message 3" link with the data-turbo-frame="third-frame" attribute which makes the link target the turbo frame with id "third-frame", inside the _MessageView3 partial view.

Lastly, we should note that when we are navigating frames, the browser history does not change, in other words, it is not a visit to a URL. At this point, I would advise you to read the “Navigate with Turbo Drive” section in the Turbo documentation to thoroughly understand the page navigation basics of Turbo. And now, let’s see this case in action. While the developer tool “Console” tab is open, click the “Go to the Quote Editor Button” and you should see the turbo:visit log entry in the console that we have hooked to that event in the app.js file. We also see that the URL has changed from https://localhost:7086/ to https://localhost:7086/Tutorial/Quotes.

Turbo drive “Application Visit”

But when we click either the “Edit Me” or “Add Message 3” buttons, we will see that the URL does not change, and instead of turbo:visit, turbo:frame-load event is logged.

Turbo frame navigation

_top frame

Until now, we have seen the links or the forms targeting the turbo frame they are in or another turbo frame by adding the data-turbo-frame attribute. What if we don’t want to target any other frame, and instead target the entire page itself? In that case, we set the target="_top" attribute on that frame. _top is not really a turbo frame, but it acts like a turbo frame to represent the whole page. So, when we navigate from a turbo frame that has target="_top" attribute set, Turbo will not be looking for a turbo frame with the same id in the response; instead, the response will replace the whole page. Similarly, we might want to achieve the same thing on our links which are inside a turbo frame. In that case, we set the data-turbo-frame attribute on these non-frame elements to "_top".

Summary

In this part, we have covered how Turbo works by working with its two components: Turbo Drive and Turbo Frame. In part 4, we will cover Turbo Streams. You can download the code covering this part from here.

References

[1] Navigate with Turbo Drive
[2] Turbo Drive
[3] Decompose with Turbo Frames
[4] Turbo Frames and Turbo Stream templates
[5] Areas in ASP.NET Core

--

--