├── .config └── dotnet-tools.json ├── .gitignore ├── .run └── Start & End.run.xml ├── Exercises ├── Exercises.End │ ├── Exercises.End.csproj │ ├── Models │ │ └── ServerEventsWorker.cs │ ├── Pages │ │ ├── 01_HelloWorld.cshtml │ │ ├── 01_HelloWorld.cshtml.cs │ │ ├── 02_Counter.cshtml │ │ ├── 02_Counter.cshtml.cs │ │ ├── 03_Selects.cshtml │ │ ├── 03_Selects.cshtml.cs │ │ ├── 04_Search.cshtml │ │ ├── 04_Search.cshtml.cs │ │ ├── 05_Scroll.cshtml │ │ ├── 05_Scroll.cshtml.cs │ │ ├── 06_Modal.cshtml │ │ ├── 06_Modal.cshtml.cs │ │ ├── 07_Tabs.cshtml │ │ ├── 07_Tabs.cshtml.cs │ │ ├── 08_Shortcuts.cshtml │ │ ├── 08_Shortcuts.cshtml.cs │ │ ├── 09_FormValidation.cshtml │ │ ├── 09_FormValidation.cshtml.cs │ │ ├── 10_Polling.cshtml │ │ ├── 10_Polling.cshtml.cs │ │ ├── 11_ServerEvents.cshtml │ │ ├── 11_ServerEvents.cshtml.cs │ │ ├── 12_TagHelpers.cshtml │ │ ├── 12_TagHelpers.cshtml.cs │ │ ├── 13_ClientsideTemplates.cshtml │ │ ├── 13_ClientsideTemplates.cshtml.cs │ │ ├── 14_OutOfBand.cshtml │ │ ├── 14_OutOfBand.cshtml.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── Shared │ │ │ ├── Pagination.cshtml │ │ │ ├── _Cards.cshtml │ │ │ ├── _Form.cshtml │ │ │ ├── _Layout.cshtml │ │ │ ├── _Modal.cshtml │ │ │ ├── _Results.cshtml │ │ │ ├── _ShoppingItem.cshtml │ │ │ ├── _Stonks.cshtml │ │ │ ├── _Tabs.cshtml │ │ │ └── _Toast.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── fontawesome │ │ │ ├── all.css │ │ │ └── all.min.css │ │ └── webfonts │ │ │ ├── fa-brands-400.eot │ │ │ ├── fa-brands-400.svg │ │ │ ├── fa-brands-400.ttf │ │ │ ├── fa-brands-400.woff │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.eot │ │ │ ├── fa-regular-400.svg │ │ │ ├── fa-regular-400.ttf │ │ │ ├── fa-regular-400.woff │ │ │ ├── fa-regular-400.woff2 │ │ │ ├── fa-solid-900.eot │ │ │ ├── fa-solid-900.svg │ │ │ ├── fa-solid-900.ttf │ │ │ ├── fa-solid-900.woff │ │ │ └── fa-solid-900.woff2 │ │ ├── img │ │ ├── Pacman.gif │ │ ├── bars.svg │ │ ├── jetbrains.png │ │ ├── joystick.svg │ │ └── search.svg │ │ └── js │ │ ├── _hyperscript.min.js │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ ├── bootstrap.min.js.map │ │ ├── extra │ │ ├── client-side-templates.js │ │ └── mustache.js │ │ └── htmx.min.js └── Exercises.Start │ ├── Exercises.Start.csproj │ ├── Models │ └── ServerEventsWorker.cs │ ├── Pages │ ├── 01_HelloWorld.cshtml │ ├── 01_HelloWorld.cshtml.cs │ ├── 02_Counter.cshtml │ ├── 02_Counter.cshtml.cs │ ├── 03_Selects.cshtml │ ├── 03_Selects.cshtml.cs │ ├── 04_Search.cshtml │ ├── 04_Search.cshtml.cs │ ├── 05_Scroll.cshtml │ ├── 05_Scroll.cshtml.cs │ ├── 06_Modal.cshtml │ ├── 06_Modal.cshtml.cs │ ├── 07_Tabs.cshtml │ ├── 07_Tabs.cshtml.cs │ ├── 08_Shortcuts.cshtml │ ├── 08_Shortcuts.cshtml.cs │ ├── 09_FormValidation.cshtml │ ├── 09_FormValidation.cshtml.cs │ ├── 10_Polling.cshtml │ ├── 10_Polling.cshtml.cs │ ├── 11_ServerEvents.cshtml │ ├── 11_ServerEvents.cshtml.cs │ ├── 12_TagHelpers.cshtml │ ├── 12_TagHelpers.cshtml.cs │ ├── 13_ClientsideTemplates.cshtml │ ├── 13_ClientsideTemplates.cshtml.cs │ ├── 14_OutOfBand.cshtml │ ├── 14_OutOfBand.cshtml.cs │ ├── Index.cshtml │ ├── Index.cshtml.cs │ ├── Shared │ │ ├── Pagination.cshtml │ │ ├── _Cards.cshtml │ │ ├── _Form.cshtml │ │ ├── _Layout.cshtml │ │ ├── _Modal.cshtml │ │ ├── _Results.cshtml │ │ ├── _ShoppingItem.cshtml │ │ ├── _Stonks.cshtml │ │ ├── _Tabs.cshtml │ │ └── _Toast.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ ├── css │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── fontawesome │ │ ├── all.css │ │ └── all.min.css │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ ├── img │ ├── Pacman.gif │ ├── bars.svg │ ├── jetbrains.png │ ├── joystick.svg │ └── search.svg │ └── js │ ├── _hyperscript.min.js │ ├── bootstrap.js │ ├── bootstrap.js.map │ ├── bootstrap.min.js │ ├── bootstrap.min.js.map │ ├── extra │ ├── client-side-templates.js │ └── mustache.js │ └── htmx.min.js ├── JetSwagStore ├── JetSwagStore.Models │ ├── Category.cs │ ├── Extensions │ │ └── QueryableExtensions.cs │ ├── JetSwagStore.Models.csproj │ ├── Migrations │ │ ├── 20210903142041_Initial.Designer.cs │ │ ├── 20210903142041_Initial.cs │ │ └── StoreDbContextModelSnapshot.cs │ ├── Order.cs │ ├── OrderItem.cs │ ├── Product.cs │ ├── ProductOption.cs │ ├── ShoppingCart.cs │ ├── ShoppingCartItem.cs │ └── StoreDbContext.cs └── JetSwagStore.Web │ ├── Controllers │ ├── CartController.cs │ ├── HomeController.cs │ └── ProductsController.cs │ ├── JetSwagStore.Web.csproj │ ├── Models │ ├── Cart │ │ ├── CurrentShoppingCart.cs │ │ ├── ShoppingCartExtensions.cs │ │ ├── ShoppingCartMiddleware.cs │ │ ├── ShoppingCartViewModelFilter.cs │ │ └── UpdateCartRequest.cs │ ├── Components │ │ └── Categories │ │ │ ├── CategoriesViewComponent.cs │ │ │ ├── CategoriesViewModel.cs │ │ │ └── CategoryViewModel.cs │ ├── ErrorViewModel.cs │ ├── Home │ │ ├── CartViewModel.cs │ │ ├── IndexViewModel.cs │ │ ├── ProductViewModel.cs │ │ └── ProductWithOptionsViewModel.cs │ └── ShoppingRazorPage.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Views │ ├── Home │ │ ├── Index.cshtml │ │ ├── Privacy.cshtml │ │ └── _Products.cshtml │ ├── Shared │ │ ├── Components │ │ │ └── Categories │ │ │ │ └── Categories.cshtml │ │ ├── Error.cshtml │ │ ├── _CartButton.cshtml │ │ ├── _CartItems.cshtml │ │ ├── _Layout.cshtml │ │ ├── _Product.cshtml │ │ └── _ProductOptions.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ ├── css │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── fontawesome │ │ ├── all.css │ │ └── all.min.css │ ├── site.css │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ ├── favicon.ico │ ├── img │ ├── AppCode.jpg │ ├── DataGrip.jpg │ ├── DotMemory.jpg │ ├── DotPeek.jpg │ ├── DotTrace.jpg │ ├── IntelliJ.jpg │ ├── ReSharper.jpg │ ├── Rider.jpg │ ├── WebStorm.jpg │ ├── header.jpg │ └── jetbrains.png │ └── js │ ├── _hyperscript.min.js │ ├── bootstrap.js │ ├── bootstrap.js.map │ ├── bootstrap.min.js │ ├── bootstrap.min.js.map │ ├── extra │ ├── client-side-templates.js │ └── mustache.js │ ├── htmx.min.js │ └── site.js ├── LICENSE ├── global.json ├── htmx-aspnetcore.sln └── readme.md /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "6.0.0-preview.7.21378.4", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.run/Start & End.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Exercises/Exercises.End/Exercises.End.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | Exercises 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Exercises/Exercises.End/Models/ServerEventsWorker.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using Lib.AspNetCore.ServerSentEvents; 3 | 4 | namespace Exercises.Models 5 | { 6 | public class ServerEventsWorker : BackgroundService 7 | { 8 | private readonly IServerSentEventsService client; 9 | 10 | public ServerEventsWorker(IServerSentEventsService client) 11 | { 12 | this.client = client; 13 | } 14 | 15 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 16 | { 17 | try 18 | { 19 | while (!stoppingToken.IsCancellationRequested) 20 | { 21 | var clients = client.GetClients(); 22 | if (clients.Any()) 23 | { 24 | Number.Value = RandomNumberGenerator.GetInt32(1, 100); 25 | await client.SendEventAsync( 26 | new ServerSentEvent 27 | { 28 | Id = "number", 29 | Type = "number", 30 | Data = new List 31 | { 32 | Number.Value.ToString() 33 | } 34 | }, 35 | stoppingToken 36 | ); 37 | } 38 | 39 | await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); 40 | } 41 | } 42 | catch (TaskCanceledException) 43 | { 44 | // this exception is expected 45 | } 46 | } 47 | } 48 | 49 | public static class Number 50 | { 51 | public static int Value { get; set; } = 1; 52 | } 53 | } -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/01_HelloWorld.cshtml: -------------------------------------------------------------------------------- 1 | @page "/examples/01-helloworld" 2 | @using Exercises.Models 3 | @model Exercises.Pages.HelloWorld 4 | @{ 5 | ViewBag.Title = "Hello World"; 6 | ViewBag.Home = true; 7 | ViewBag.Next = "02_Counter"; 8 | } 9 | 10 | 11 | # 1. Hello World! 12 | 13 | HTMX allows you to decorate any HTML element with attributes that will trigger an HTTP request. Then, once the page receives a response from the server, HTMX will use the HTML response to replace a part of the existing page. 14 | 15 | As a starting point, let's replace the **Change Me!** text below with **Hello, World!**. 16 | 17 | **Hint:** You'll need the attributes of `hx-get` and `hx-target`. You can 18 | also leave the value of `hx-get` empty, which will get this page, or use `Url.Page("01_HelloWorld")` to generate a link. 19 | 20 | --- 21 | 22 | 23 | 24 |
25 |

26 | Change Me! 27 |

28 | 29 | 30 | 31 | 37 |
38 | 39 | 40 | 41 | --- 42 | 43 | **Bonus:** You'll notice the `PageModel` of `HelloWorld` is using HTMX.NET to determine whether the incoming request is an HTMX request. 44 | 45 | ```c# 46 | return Request.IsHtmx() 47 | ? Content("Hello, World!", "text/html") 48 | : Page(); 49 | ``` 50 | 51 | This is a common pattern you'll see through out this project. 52 | 53 | 54 | -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/01_HelloWorld.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Htmx; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Exercises.Pages 6 | { 7 | public class HelloWorld : PageModel 8 | { 9 | public IActionResult OnGet() 10 | { 11 | return Request.IsHtmx() 12 | ? Content( 13 | // language=html 14 | "Hello, World!", "text/html" 15 | ) 16 | : Page(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/02_Counter.cshtml: -------------------------------------------------------------------------------- 1 | @page "/examples/02-counter" 2 | @model Exercises.Pages.Counter 3 | @{ 4 | ViewBag.Title = "Counters"; 5 | ViewBag.Previous = "01_HelloWorld"; 6 | ViewBag.Next = "03_Selects"; 7 | } 8 | 9 | 10 | # 2. Counters! 11 | 12 | We retrieved a static HTML snippet in the previous sample and replaced an element on the page, but what about maintaining the application state? We'll increment a counter on the server in this sample while updating our UI to reflect the change. This time around, we'll use the `POST` action to show that HTMX supports most HTTP methods. 13 | 14 | To accomplish the incrementing feature, we'll need to modify the following `button`. Similar to our `Hello, World` sample, it's a matter of a few attributes. The only difference is that we have a bit more server-side logic involved in maintaining the current count in this case. 15 | 16 | Give it a try! 17 | 18 | **Hint: You'll need to use `hx` attributes of `hx-post` and `hx-target`. For the backend ASP.NET Core code, check out the `1_Counter.cshtml.cs` file.** 19 | 20 | 21 |
22 |
23 |
24 | 25 | 31 |
32 |
33 |

34 | 0 35 |

36 |
37 |
38 |
39 | 40 | 41 | **Note: We reset the count on page refreshes since we store the value in a static variable. Storage is an implementation detail, and you could keep the incremented value in a database for less volatility.** 42 | -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/02_Counter.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace Exercises.Pages 5 | { 6 | public class Counter : PageModel 7 | { 8 | private static int count = 0; 9 | 10 | public void OnGet() 11 | { 12 | // reset on refresh 13 | count = 0; 14 | } 15 | 16 | public IActionResult OnPost() 17 | { 18 | // TODO: Increment the count on each request 19 | return Content($"{++count}", "text/html"); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/03_Selects.cshtml: -------------------------------------------------------------------------------- 1 | @page "/examples/03-selects" 2 | @model Exercises.Pages.Selects 3 | @{ 4 | ViewBag.Title = "Selects"; 5 | ViewBag.Previous = "02_Counter"; 6 | ViewBag.Next = "04_Search"; 7 | } 8 | 9 | 10 | # 3. Selects (Dropdowns) 11 | 12 | The select HTML element is one of the most commonly used in UX development, allowing users to select from a predetermined list of options. ASP.NET Core developer may also refer to the element as a **dropdown**, a holdover from the ASP.NET WebForm era. Regardless of what you call them, they are fantastic. 13 | 14 | One of the most common patterns with select elements is cascading selects. The general idea is that a selection in one component affects the visible values in another. Stay around in web development long enough, and you'll likely have to implement this pattern. With HTMX, you can accomplish the described behavior with a few attributes and an ASP.NET Core endpoint. 15 | 16 | Instead of returning all the results of the select elements and filtering them on the client, we can lean on ASP.NET Core to perform the complex logic and only return the required information. 17 | 18 | **Hint: to accomplish this, you'll need to add `hx-get` on both select dropdowns. You can also complete this sample by revealing each dropdown as values are selected. Experiment and to see what feels more natural.** 19 | 20 | 21 |
22 |
23 | 24 | 30 |
31 |
32 | 33 | 38 |
39 | 40 | 46 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/03_Selects.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Html; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.AspNetCore.Mvc.Rendering; 8 | 9 | namespace Exercises.Pages 10 | { 11 | public class Selects : PageModel 12 | { 13 | private readonly Dictionary> cuisines = 14 | new() 15 | { 16 | {"Italian", new List {"Spaghetti", "Pizza", "Lasagna"}}, 17 | {"Mexican", new List {"Tacos", "Enchiladas", "Churros"}}, 18 | {"American", new List {"Burgers", "Hot dogs", "Barbeque"}} 19 | }; 20 | 21 | public IList CuisineItems 22 | { 23 | get 24 | { 25 | var items = cuisines.Keys 26 | .Select(c => new SelectListItem(c, c)) 27 | .ToList(); 28 | 29 | items.Insert(0, new SelectListItem("Choose an option", "") { 30 | Disabled = true, 31 | Selected = true 32 | }); 33 | 34 | return items; 35 | } 36 | } 37 | 38 | [BindProperty(SupportsGet = true)] 39 | public string? Cuisine { get; set; } 40 | 41 | [BindProperty(SupportsGet = true)] 42 | public string? Food { get; set; } 43 | 44 | public void OnGet() 45 | { 46 | } 47 | 48 | public IActionResult OnGetFoods() 49 | { 50 | var html = new StringBuilder(); 51 | if (Cuisine is { Length: > 0 } cuisine && cuisines.TryGetValue(cuisine, out var foods)) 52 | { 53 | html.AppendLine(""); 54 | foreach (var food in foods) 55 | { 56 | html.AppendLine($""); 57 | } 58 | } 59 | 60 | return Content(html.ToString(), "text/html"); 61 | } 62 | 63 | public IActionResult OnGetLove() 64 | { 65 | return Content($" I love {Food}!"); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/04_Search.cshtml: -------------------------------------------------------------------------------- 1 | @page "/examples/04-search" 2 | @model Exercises.Pages.Search 3 | 4 | @{ 5 | ViewBag.Title = "Search"; 6 | ViewBag.Previous = "03_Selects"; 7 | ViewBag.Next = "05_Scroll"; 8 | } 9 | 10 | 11 | # 4. Search 12 | 13 | Search is a fundamental part of all applications. Empowering your users to find any data within a system with a few keystrokes can transform how your users view your application. A great user experience for search is almost as necessary as the search results in themselves. With HTMX, we can lean on all the `input` events available to increase the responsiveness of our search experience. 14 | 15 | The following example will show how to transform a tedious input into a debounced search-as-you-type experience. You'll be using the `hx-trigger` attribute with keywords like `keyup`, `changed`, and `delay:250ms` to hold a query until a user has stopped typing. 16 | 17 | On the ASP.NET Core side, we'll search an existing collection using the `Query` property. Note how the URL changes as the back-end code handle the client requests. We are utilizing HTMX response headers to push new URLs into the client's history. A powerful and necessary feature as we continue to build more complex user experiences. 18 | 19 | You may also notice we have begun utilizing ASP.NET Core partial views. You must break down your user experience in terms of components to help create smaller Razor partials. In a worst-case scenario, you can lean on rendering the entire page, but in that case, you would lose most of the benefits of HTMX. It takes some practice, but I believe anyone can learn this skill. As always, there are many ways to solve a problem, so experiment and have fun. 20 | 21 | --- 22 | 23 | 24 |
25 |

26 | joystick 27 | Retro Games 28 | search 29 |

30 |
31 | 32 | search 33 | 34 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | @await Html.PartialAsync("_Results") 59 | 60 |
YearPublisherConsoleName
61 |
62 | 63 | 64 | **Note: This is the most sophisticated UI experience so far, and yet there's not much to it. Amazing, right?!** 65 | 66 | 67 | @section head { 68 | 75 | } -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/04_Search.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Htmx; 6 | using Microsoft.AspNetCore.Http.Extensions; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Westwind.AspNetCore.Markdown.Utilities; 10 | 11 | namespace Exercises.Pages 12 | { 13 | public class Search : PageModel 14 | { 15 | private static List Games = new() 16 | { 17 | new (1993, "Super Mario Bros. 3", "Nintendo", "NES"), 18 | new (1992, "The Legend of Zelda: A Link To The Past", "Nintendo", "SNES"), 19 | new (1992, "Street Fighter II Turbo", "Capcom", "Arcade"), 20 | new (1992, "Sonic The Hedgehog 2", "Sega", "Mega Drive"), 21 | new (1986, "Outrun", "Sega", "Arcade"), 22 | new (1978, "Space Invaders", "Taito", "Arcade"), 23 | new (1992, "Streets Of Rage 2", "Sega", "Mega Drive"), 24 | new (1994, "Super Metroid", "Nintendo", "SNES"), 25 | new (1972, "Pong", "Atari", "Atari"), 26 | new (1996, "Resident Evil", "Capcom", "Playstation"), 27 | new (1995, "Chrono Trigger", "Squaresoft", "SNES") 28 | }; 29 | 30 | [BindProperty(SupportsGet = true)] 31 | public string? Query { get; set; } 32 | 33 | public List Results { get; private set; } 34 | = Games; 35 | 36 | public IActionResult OnGet() 37 | { 38 | Results = string.IsNullOrEmpty(Query) 39 | ? Games 40 | : Games.Where(g => g.ToString().Contains(Query, StringComparison.OrdinalIgnoreCase)).ToList(); 41 | 42 | if (!Request.IsHtmx()) 43 | return Page(); 44 | 45 | Response.Htmx(h => { 46 | // we want to push the current url 47 | // into the history 48 | h.Push(Request.GetEncodedUrl()); 49 | }); 50 | 51 | return Partial("_Results", this); 52 | } 53 | } 54 | 55 | public record Game(int Year, string Name, string Publisher, string Console); 56 | } -------------------------------------------------------------------------------- /Exercises/Exercises.End/Pages/05_Scroll.cshtml: -------------------------------------------------------------------------------- 1 | @page "/examples/05-scroll" 2 | @model Exercises.Pages.Scroll 3 | 4 | @{ 5 | ViewBag.Title = "Scroll"; 6 | ViewBag.Previous = "04_Search"; 7 | ViewBag.Next = "06_Modal"; 8 | } 9 | 10 | 11 | # 5. Infinite Scroll 12 | 13 | The user experience is about giving our users as much information as they can absorb, no more and no less. You can use paging as a technique to feed more information to a user once they've processed what's you already displayed to them. In modern web development, there are two common styles of paging: 14 | 15 | - Manual paging, where your user clicks to get the next set of results. 16 | - Infinite scrolling, commonly used on social media sites when the site has chronologically ordered the results. 17 | 18 | In this sample, we'll show the more exciting **infinite scrolling** mechanic. HTMX supports the `revealed` trigger, which the client raises when an element is visible in the UI. You'll use this event to append a new set of results to the current results. **Note, in this sample, the results will scroll forever. So you'll never reach the bottom, but you can certainly try.** 19 | 20 | Let's look at the attributes we'll apply to the last element of our currently visible result set. You'll need some Razor logic to apply the attributes only once. Look in the files `05_Scroll.cshtml` and `Shared/_Card.cshtml` to see implementation of this sample. 21 | 22 | ``` 23 | @* ReSharper disable Html.TagNotClosed *@ 24 |