Skip to content

Commit 2f3a04d

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat-htmxor-component-result
2 parents 259c2a1 + c7543bb commit 2f3a04d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1017
-73
lines changed

Htmxor.sln

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazingPizza", "samples\Bla
2727
EndProject
2828
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalHtmxorApp", "samples\MinimalHtmxorApp\MinimalHtmxorApp.csproj", "{3A90B20F-9E9B-454D-9EF9-72B10D4D1F54}"
2929
EndProject
30+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmxorExamples", "samples\HtmxorExamples\HtmxorExamples.csproj", "{2B36AAB9-AD16-4D46-8FB5-D78EDC003F90}"
31+
EndProject
3032
Global
3133
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3234
Debug|Any CPU = Debug|Any CPU
@@ -53,6 +55,10 @@ Global
5355
{3A90B20F-9E9B-454D-9EF9-72B10D4D1F54}.Debug|Any CPU.Build.0 = Debug|Any CPU
5456
{3A90B20F-9E9B-454D-9EF9-72B10D4D1F54}.Release|Any CPU.ActiveCfg = Release|Any CPU
5557
{3A90B20F-9E9B-454D-9EF9-72B10D4D1F54}.Release|Any CPU.Build.0 = Release|Any CPU
58+
{2B36AAB9-AD16-4D46-8FB5-D78EDC003F90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59+
{2B36AAB9-AD16-4D46-8FB5-D78EDC003F90}.Debug|Any CPU.Build.0 = Debug|Any CPU
60+
{2B36AAB9-AD16-4D46-8FB5-D78EDC003F90}.Release|Any CPU.ActiveCfg = Release|Any CPU
61+
{2B36AAB9-AD16-4D46-8FB5-D78EDC003F90}.Release|Any CPU.Build.0 = Release|Any CPU
5662
EndGlobalSection
5763
GlobalSection(SolutionProperties) = preSolution
5864
HideSolutionNode = FALSE
@@ -63,6 +69,7 @@ Global
6369
{0347D883-0078-4F68-BA24-DA7E3004D31D} = {BB63193F-EEC0-4EE6-B89E-9B4E1983E2FF}
6470
{59324821-EF01-40F6-9674-81094C4229A7} = {8D4A2D7D-532C-4B68-B5D0-17873D49FB0D}
6571
{3A90B20F-9E9B-454D-9EF9-72B10D4D1F54} = {8D4A2D7D-532C-4B68-B5D0-17873D49FB0D}
72+
{2B36AAB9-AD16-4D46-8FB5-D78EDC003F90} = {8D4A2D7D-532C-4B68-B5D0-17873D49FB0D}
6673
EndGlobalSection
6774
GlobalSection(ExtensibilityGlobals) = postSolution
6875
SolutionGuid = {430F7B4E-864E-44B7-84A0-A903E7386E3B}

README.md

+5-8
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@ retaining all the advantages of Blazor SSR stateless nature.
1111

1212
**Nuget:** https://www.nuget.org/packages/Htmxor
1313

14+
## Documentation
15+
16+
See https://github.com/egil/Htmxor/blob/main/docs/index.md.
17+
1418
## Samples
1519

1620
The following Blazor Web Apps (Htmxor) are used to test Htmxor and demo the capabilities of it.
1721

1822
- [Blazing Pizza workshop as Htmxor App](https://github.com/egil/Htmxor/tree/main/samples/BlazingPizza)
19-
- [Htmxor - TestApp](https://github.com/egil/Htmxor/tree/main/test/Htmxor.TestApp)
23+
- [Htmxor Examples](https://github.com/egil/Htmxor/tree/main/samples/HtmxorExamples)
2024
- [Minimal Htmxor App template](https://github.com/egil/Htmxor/tree/main/samples/MinimalHtmxorApp)
21-
22-
## Documentation
23-
24-
- **[Getting Started](https://github.com/egil/Htmxor/blob/main/docs/getting-started.md)** - how to create a new Htmxor/Blazor project.
25-
- **[Routing in Htmxor](https://github.com/egil/Htmxor/blob/main/docs/routing.md)** - there are two types of routing in Htmxor, standard and direct.
26-
- **[Routing in Htmxor](https://github.com/egil/Htmxor/blob/main/docs/routing.md)** - there are two types of routing in Htmxor, standard and direct.
27-

docs/getting-started.md docs/index.md

+87-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
# Getting Started
1+
# Htmxor
2+
3+
[add introduction]
4+
5+
## Getting Started
26

37
To create a minimal Blazor + htmx app with various examples, download the [Minimal Htmxor App template](https://github.com/egil/Htmxor/tree/main/samples/MinimalHtmxorApp).
48

5-
To start fresh, follow these steps:
9+
To start fresh from a (new) Blazor Web App project, follow these steps:
610

711
1. **Add the Htmxor Package**
812

@@ -69,7 +73,7 @@ To start fresh, follow these steps:
6973
+ <body hx-boost="true">
7074
<Routes />
7175

72-
- <script src="_framework/blazor.web.js"></script>
76+
- <script src="_framework/blazor.web.js"></script>
7377
</body>
7478

7579
</html>
@@ -108,3 +112,83 @@ To start fresh, follow these steps:
108112
```
109113

110114
Note that we set up the custom layout for all components by defining the `[HtmxLayout(typeof(HtmxorLayout))]` attribute in the `_Imports.razor` file.
115+
116+
## Routing in Htmxor
117+
118+
Htmxor routing and Blazor Static Web Apps routing differ in ways that enhance htmx scenarios. In Htmxor, there are two types of routing:
119+
120+
In Htmxor, there are **two** types of routing:
121+
122+
- **Standard routing**
123+
- **Direct routing**
124+
125+
The routing mode is determined by the presence or absence of [htmx headers](https://htmx.org/reference/#request_headers):
126+
127+
```
128+
if ( HX-Request is null || ( HX-Boosted is not null && HX-Target is null ) )
129+
RoutingMode.Standard
130+
else
131+
RoutingMode.Direct
132+
```
133+
134+
Here's a detailed look at each mode:
135+
136+
### Standard Routing
137+
138+
Standard routing is used when the `HX-Request` header is missing, or when `HX-Boosted` is present and `HX-Target` is missing.
139+
140+
In this mode, routing behaves like conventional Blazor Static Web Apps routing. The root component (typically App.razor or the component passed to `MapRazorComponents<TRootComponent>()` in `Program.cs`) is rendered.
141+
142+
The root component usually renders a `<Router>` component that determines which `@page`-annotated component to render based on the HTTP request, using the layout specified for that page.
143+
144+
Example:
145+
146+
```
147+
HTTP GET /my-page
148+
App --> Routes --> MainLayout --> MyPage
149+
```
150+
151+
### Direct Routing
152+
153+
Direct routing bypasses the root component (`App.razor`) and the standard layout (`MainLayout`). Instead, it routes directly to the component that matches the request.
154+
155+
If the target component has a `HtmxLayout` attribute, that layout is rendered first.
156+
157+
Example:
158+
159+
```
160+
HTTP GET /my-htmx-page-with-layout
161+
HtmxLayout --> MyHtmxPageWithHtmxLayout
162+
163+
HTTP GET /my-htmx-page
164+
MyHtmxPage
165+
```
166+
167+
This allows `MyHtmxPage` to be rendered directly, optionally including a specified `HtmxLayout`.
168+
169+
## Conditional Rendering aka. Template Fragments
170+
171+
In Htmxor, conditional rendering supports the [template fragments](https://htmx.org/essays/template-fragments/) pattern.
172+
173+
It allows a single routable component to render specific parts for particular requests or the full content for others. This way, you can keep all related fragments within a single component, avoiding the need to split them into separate, individually routable components.
174+
175+
By consolidating the HTML into one file, it becomes easier to understand feature functionality, adhering to the [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour/) design principle.
176+
177+
### Enabling Conditional Rendering
178+
179+
TODO:
180+
181+
- ConditionalComponentBase
182+
- IConditionalRender
183+
184+
## Layouts
185+
186+
TODO:
187+
188+
- HtmxLayout
189+
190+
## Events Handlers
191+
192+
TODO:
193+
194+
- How handlers are associated with requests

docs/routing.md

-52
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<base href="/" />
8+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
9+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous" defer></script>
10+
<link rel="stylesheet" href="app.css" />
11+
<link rel="stylesheet" href="HtmxorExamples.styles.css" />
12+
<HtmxHeadOutlet />
13+
<HeadOutlet />
14+
</head>
15+
16+
<body>
17+
<Routes />
18+
</body>
19+
20+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Htmxor.Http;
3+
using Microsoft.AspNetCore.Components;
4+
using Microsoft.AspNetCore.Components.Rendering;
5+
6+
namespace Htmxor.Components;
7+
8+
public class HtmxFragment : ConditionalComponentBase
9+
{
10+
[Parameter, EditorRequired]
11+
public required RenderFragment ChildContent { get; set; }
12+
13+
[Parameter]
14+
public Func<HtmxRequest, bool>? Match { get; set; }
15+
16+
[Parameter]
17+
public bool OnStandardRequest { get; set; } = true;
18+
19+
protected override void BuildRenderTree(RenderTreeBuilder builder)
20+
{
21+
if (ShouldOutput(Context, 0, 0))
22+
{
23+
builder.AddContent(0, ChildContent);
24+
}
25+
}
26+
27+
public override bool ShouldOutput([NotNull] HtmxContext context, int directConditionalChildren, int conditionalChildren)
28+
=> (OnStandardRequest && context.Request.RoutingMode is RoutingMode.Standard)
29+
|| (Match?.Invoke(context.Request) ?? true);
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Htmxor.Http;
3+
using Microsoft.AspNetCore.Components;
4+
using Microsoft.AspNetCore.Components.Rendering;
5+
6+
namespace Htmxor.Components;
7+
8+
public class HtmxFragmentElement : HtmxFragment
9+
{
10+
[Parameter(CaptureUnmatchedValues = true)]
11+
public IDictionary<string, object>? AdditionalAttributes { get; set; }
12+
13+
/// <summary>
14+
/// The ID of the element that <see cref="HtmxAsyncLoad"/> should render it's content inside.
15+
/// This is also the ID that htmx will use to target the element to replace it's content.
16+
/// </summary>
17+
[Parameter, EditorRequired]
18+
public required string Id { get; set; }
19+
20+
/// <summary>
21+
/// The element type that <see cref="HtmxAsyncLoad"/> should render
22+
/// <see cref="ChildContent"/> and <see cref="Loading"/> into.
23+
/// </summary>
24+
/// <remarks>Default is a <c>div</c> element.</remarks>
25+
[Parameter]
26+
public string Element { get; set; } = "div";
27+
28+
public override bool ShouldOutput([NotNull] HtmxContext context, int directConditionalChildren, int conditionalChildren)
29+
=> (OnStandardRequest && context.Request.RoutingMode is RoutingMode.Standard)
30+
|| (Match?.Invoke(context.Request) ?? context.Request.Target == Id);
31+
32+
protected override void OnParametersSet()
33+
{
34+
Element = string.IsNullOrWhiteSpace(Element) ? "div" : Element.Trim();
35+
Id = string.IsNullOrWhiteSpace(Id) ? Id : Id.Trim();
36+
37+
if (AdditionalAttributes is null)
38+
{
39+
return;
40+
}
41+
42+
RemoveControlledAttributeAndThrow(AdditionalAttributes, Constants.Attributes.HxTarget);
43+
RemoveControlledAttributeAndThrow(AdditionalAttributes, Constants.Attributes.HxSwap);
44+
}
45+
46+
protected override void BuildRenderTree([NotNull] RenderTreeBuilder builder)
47+
{
48+
if (!ShouldOutput(Context, 0, 0))
49+
{
50+
return;
51+
}
52+
53+
var request = Context.Request;
54+
builder.OpenElement(1, Element);
55+
builder.AddAttribute(2, Constants.Attributes.Id, Id);
56+
builder.AddAttribute(3, Constants.Attributes.HxSwap, Constants.SwapStyles.OuterHTML);
57+
58+
if (AdditionalAttributes is not null)
59+
{
60+
builder.AddMultipleAttributes(4, AdditionalAttributes);
61+
}
62+
63+
builder.AddContent(5, ChildContent);
64+
builder.CloseElement();
65+
}
66+
67+
private static void RemoveControlledAttributeAndThrow(IDictionary<string, object> attributes, string attributeName)
68+
{
69+
if (attributes.Remove(attributeName))
70+
{
71+
throw new ArgumentException($"The '{attributeName}' attribute is controlled by the {nameof(HtmxFragmentElement)} components and should not be set explicitly.");
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@inherits HtmxLayoutComponentBase
2+
@Body
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@inherits LayoutComponentBase
2+
<main class="container p-3">
3+
@Body
4+
</main>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#blazor-error-ui {
2+
background: lightyellow;
3+
bottom: 0;
4+
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
5+
display: none;
6+
left: 0;
7+
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
8+
position: fixed;
9+
width: 100%;
10+
z-index: 1000;
11+
}
12+
13+
#blazor-error-ui .dismiss {
14+
cursor: pointer;
15+
position: absolute;
16+
right: 0.75rem;
17+
top: 0.5rem;
18+
}

0 commit comments

Comments
 (0)