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)
And finally, we are in the last part, where we will implement what we have learned so far, about Turbo and Stimulus, to develop the quote editor. I advise you to check out the original quote editor to see how it works so that you will better understand what we are going to develop.
First, we have to prepare our data, and here is a simple diagram of our classes. In the application, you can find these class definitions in the Models\Quote.cs
file.
To keep the data, I created a QuoteRepository
class and registered it as a singleton service in the Program.cs
file. QuoteRepository
class populates a static list with predefined entities in its constructor.
builder.Services.AddSingleton<IRepository<Quote>, QuoteRepository>();
The quote editor has two main pages:
Quotes.cshtml
: Lists the quotes with their names.
- We can add\edit\delete aQuote
on this page.QuoteDetail.cshtml
: Shows a quote with Date and Item details.
- We can add\edit\delete aQuoteDate
on this page.
- We can add\edit\delete aQuoteItem
on this page.
These two pages are shown in blue in the below diagram:
As you noticed, a Quote
, QuoteDate
, or a QuoteItem
have independent CRUD actions which can be executed inline on a single page, thanks to Turbo. But if you remember, in Part 4, I mentioned that my implementation was dependent on turbo streams, but it was advised that we should develop the application even when the turbo was not available. In my quote editor implementation, I decided to follow this suggestion and therefore needed to add three more pages instead of using partial views as before:
Quote.cshtml
QuoteDate.cshtml
QuoteItem.cshtml
When turbo is available, we will use but not navigate to these pages; the navigation will only be between the above mentioned Quotes.cshtml
and QuoteDetail.cshtml
pages.
Quotes page and adding, editing a quote
Below is the Quotes.cshtml
main page code which shows the quotes as a list.
And below is the simplified markup of the Quotes
page which has the “Add Quote” button and three quotes. The three quotes are rendered inside a turbo frame shown in green. The “Add Quote” button is not inside a turbo frame, but it targets the quote_0
turbo frame using a data-turbo-frame
attribute.
The quote_0
turbo frame, shown in blue, is rendered as an empty turbo frame which exists for replacement with an editor. But if you check out the code above, if there are no quotes then we render the Quote\_Empty.cshtml
partial view inside the quote_0
turbo frame which renders the following view:
Also notice the empty div
tag with id notification
and the turbo frame with id quotes
(surrounding the all the quotes) in yellow. We will soon mention them.
Now, let’s look at the flow of interactions for a Quote instance:
- Inside the
Quotes.cshtml
page, we use theQuote\_View.cshtml
partial view to show each quote by rendering its name with a delete and edit button.
2. When we click the Add Quote button, the OnGetAdd
named handler of Quote.cshtml
page is called and it returns the Quote.cshtml
page. And similarly, if we click the edit button, the OnGetEdit
named handler is called and again returns the Quote.cshtml
page.
As we can see below, the Quote.cshtml
page code renders either the Quote\_View.cshtml
or the Quote\_AddEdit.cshtml
partial view, depending on the handler route parameter defined at the top with the @page
directive.
4. If you check out these Quote\_View.cshtml
and Quote\_AddEdit.cshtml
partial views, you will see that they both have the necessary markup surrounded by the turbo frames. So, when turbo is not enabled, if we click the add quote button, we will go to the Quote.cshtml
page and the URL will change to Quote/Add/0
. But if turbo is enabled, the same full page response will be processed by turbo. And since our add and edit buttons are inside a turbo frame, as we explained in detail in part 3, ONLY content inside a matching <turbo-frame>
will be used when the page is updated.
5. After we add or edit a quote and click the save button, OnPostAdd
and OnPostEdit
handlers are called, respectively. After completing the necessary data operations successfully, we have to return a response. And this time, to support both with and without Turbo cases, we have to return two different responses. But first, we need to be able to detect if the request is accepting turbo stream responses. And if you remember from Part 4, we can check request’s accept header to do that by checking whether it contains "text/vnd.turbo-stream.html"
format or not. I have added the following two extension methods on the HttpRequest
class inside the Utils\ExtensionMethods.cs
file.
We will use the AcceptsTurboStream
method to decide our response type:
- If it returns
false
, we will redirect to theQuote\View\{id}
URL to returnQuote.cshtml
page withOnGetView
named handler. This will return a full page result by rendering theQuote\_View.cshtml
partial view. - If it returns
true
, we will broadcast theQuote\_Add.cshtml
partial view inOnPostAdd
and theQuote\_Edit.cshtml
partial view inOnPostEdit
.
Let’s analyze what Quote\_Add.cshtml
partial view contains.
Quote\_Add.cshtml
partial view does three things with three turbo-stream
custom elements:
- The first
turbo-stream
element has thetarget
attribute set tonotification
. This means that, it will target the DOM element with idnotification
and replace its content with the markup inside thetemplate
tags. Here the replacement is made with the_SuccessNotification.cshtml
partial view which is a common partial view, used after all our successful CRUD operations to show the success message for a few seconds. I will explain in detail how this partial view works, later in this part. Now let’s just focus on the turbo part.
Remember that while _Add.cshtml
partial is being processed, we know that turbo is included and we are on the Quotes
page, doing our add and edit operations with inline editors on this same page. So, for the turbo stream to work, there has to be a DOM element with id notification
to replace. And if you remember, we have seen it above, in yellow, on the rendered Quotes.cshtml
view.
2. The second turbo-stream
element replaces the content of turbo frame quote_0
with empty content which was previously replaced with form elements by _AddEdit.cshtml
.
3. And the third turbo-stream
element targets the element with id quotes
to prepend the newly added quote to the that element. We have mentioned that quotes
element above with yellow in the rendered Quotes.cshtml
view which was surrounding our all quote instances.
And Quote\_Edit.cshtml
is similar to Quote\_Add.cshtml
.
It again shows the success message with the first turbo-stream
. And the second turbo-stream
replaces the content of turbo frame quote_{id}
with Quote\View.cshtml
partial view which was previously replaced with form elements by _AddEdit.cshtml
.
We see that when the turbo is enabled, each
turbo-stream
element in our partial view responses act like puzzle pieces that will fit a specific place on the current view.
Now, let’s also go over the above OnPostAdd
named handler inside the Quote.cshtml.cs
code behind file to highlight some other important points.
Showing model state errors
We know that OnPostAdd
method is called when we save a new quote. So, the first thing this method does is to check if the model state is valid. If it’s not, it returns the same page which is Quote.cshtml
with Add
handler which will again render the Quote\_AddEdit.cshtml
. Inside this partial view, the following markup exists:
<vc:error-summary model=@Model></vc:error-summary>
ErrorSummary
is a view component defined under the Pages\Shared\Components
folder. It basically renders the model state errors if the error count is more than 0. Below images show this component’s output when we try to post a new quote without a name.
Showing the success message
We have mentioned the _SuccessNotification.cshtml
partial view before which accepts as a model a JsonMessage
instance. In the above OnPostAdd
code, we create a JsonMessage
instance and assign it to a local variable. If turbo is available, we assign this message to our Message
property, declared in our base PageModel
class (TutorialPageModel
) and decorated with the BindProperty
attribute. And while analyzing the turbo stream responses in Quote\_Add.cshtml
partial view, we have seen that _SuccessNotification
partial view was binding to this Message
property.
However, if turbo is not available, we have seen that we are not returning a partial view and instead redirecting to the Quote\View\{id}
URL. So, assigning the message to the same Message
property will not work in this case. Since HTTP is a stateless protocol, the value assigned to the Message
property in OnPostAdd
will not be available when OnGetView
is called. To solve this problem, we use TempData
which is a storage container for data that needs to be available to a separate HTTP request. And on the redirected page, in this case Quote.cshtml
, we access the TempData
as following:
Normally, TempData
only store simple types. To store complex types, we must manually serialize and deserialize the value. And I have done this, by adding two extension methods for ITempDataDictionary
interface which were used in the code above.
OnPostEdit
is very similar to OnPostAdd
, so I will not explain it here, and I will move on to examining how we delete a quote.
Deleting a quote
Whether turbo is included or not, we can delete a quote while we are on the Quotes.cshtml
main page. So, I have defined the OnPostDelete
named handler in the Quotes.cshtml
page. But similar to what we did in add and edit actions, our response changes depending on the availability of turbo.
After removing the Quote from the repository,
- If turbo is not available, we redirect to the same
Quotes.cshtml
page with default handler “List
” and make a full page reload. - And if turbo is available, then we just broadcast the
Quote\_Delete.cshtml
partial view to make necessary changes with turbo.
Here is the Quote\_Delete.cshtml
partial view code:
Similar to add and edit, the first turbo stream is there to show the success message by replacing the DOM element with id notification
. The second turbo stream is only rendered if the quote count is zero. If we have deleted the last quote and there aren’t any quotes left in the repository, we target the element with id quote_0
and replace its contents with the Quote\_Empty.cshtml
partial view, as we have also used it in the Quotes.cshtml
pages. The third turbo stream targets the deleted quote id and removes it.
Showing Quote Details
We have covered all the CRUD actions for a quote instance. If we want to work on the quote details by adding, editing, removing a QuoteDate
or QuoteItem
, we navigate to the QuoteDetail.cshtml
by clicking the link on quote name in Quote\_View.cshtml
.
Notice that, the anchor tag has the data-turbo-frame
attribute set to "_top"
. We have mentioned _top
keyword in part 3. By default, when we navigate within a frame, turbo will target just that frame. But like in this case, we might want to override this behavior. When we navigate from a turbo frame that has target="_top"
attribute set or a non-frame element with data-turbo-frame="_top"
, Turbo will not be looking for a turbo frame with the same id in the response; instead, the response will replace the whole page. So, that’s how we navigate to QuoteDetail
page while inside a turbo frame.
I won’t explain the CRUD actions for QuoteDate
and QuoteItem
. They are very similar to Quote
. Also, if you are stuck, you can access the final code from here.
Showing success notification using Stimulus
Our quote editor uses Stimulus only to show the success message. And I used the Stimulus Notification component from the Stimulus Components to do that. First I installed it as following:
npm install — save-dev stimulus-notification
Then I imported and registered it as seen below in our app.js
file.
It’s usage is very easy. We define an HTML element with data-controller
attribute set to notification
. And also set the necessary data-transition-
attributes for animation. data-notification-delay-value
attribute is the only attribute that you might want to change. It determines the delay in milliseconds before closing the notification. Inside this element, you define your notification window markup which I have omitted below but you can find in _SuccessNotification.cshtml
file.
Summary
Finally, we have implemented our Quote Editor. We even coded it as it would work even when Turbo is not available. I hope this series of articles would help anyone understand and jump start working with Turbo and Stimulus in their ASP.NET Core projects. You can access the final version of project from here.
References
[1] TempData In Razor Pages
[2] Stimulus Components
[3] Tailwind UI