From 3e521735c5086f85edfe6d32167915a5da508982 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Fri, 3 May 2024 11:49:02 +0000 Subject: [PATCH 1/2] feat: HtmxAsyncLoad component --- .../Components/Pages/Weather.razor | 18 +++- .../Components/Pages/WeatherData.razor | 40 ++++--- src/Htmxor/Components/HtmxAsyncLoad.cs | 102 ++++++++++++++++++ 3 files changed, 133 insertions(+), 27 deletions(-) create mode 100644 src/Htmxor/Components/HtmxAsyncLoad.cs diff --git a/samples/MinimalHtmxorApp/Components/Pages/Weather.razor b/samples/MinimalHtmxorApp/Components/Pages/Weather.razor index c9eddf2..afafe1f 100644 --- a/samples/MinimalHtmxorApp/Components/Pages/Weather.razor +++ b/samples/MinimalHtmxorApp/Components/Pages/Weather.razor @@ -1,9 +1,17 @@ @page "/weather" -Weather - loading data +@inherits ConditionalComponentBase +Weather

Weather

-

This component demonstrates showing data.

- - -
Loading data . . .
+

This component demonstrates showing lazy loaded data.

+ + + + Weather - Loading data, please wait ... + Loading data . . . + + + + + diff --git a/samples/MinimalHtmxorApp/Components/Pages/WeatherData.razor b/samples/MinimalHtmxorApp/Components/Pages/WeatherData.razor index 03dd913..88d589e 100644 --- a/samples/MinimalHtmxorApp/Components/Pages/WeatherData.razor +++ b/samples/MinimalHtmxorApp/Components/Pages/WeatherData.razor @@ -1,34 +1,30 @@ -@page "/weather/data" -@* This shows how the page title can be updated during an htmx request. *@ Weather - data loaded - - + + + + + + + + + + @foreach (var forecast in forecasts) + { - - - - + + + + - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
DateTemp. (C)Temp. (F)Summary@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+ } + @code { private WeatherForecast[] forecasts = []; protected override async Task OnInitializedAsync() { // Simulate asynchronous loading to demonstrate streaming rendering - await Task.Delay(500); + await Task.Delay(2000); var startDate = DateOnly.FromDateTime(DateTime.Now); var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", diff --git a/src/Htmxor/Components/HtmxAsyncLoad.cs b/src/Htmxor/Components/HtmxAsyncLoad.cs new file mode 100644 index 0000000..0e8ecc8 --- /dev/null +++ b/src/Htmxor/Components/HtmxAsyncLoad.cs @@ -0,0 +1,102 @@ +using Htmxor.Http; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Htmxor.Components; + +/// +/// A component that will render out an element with the hx-trigger="load" attribute +/// that causes htmx to issue a second request to the server to load the child content in the background. +/// This is useful when loading the child content takes a long time and you want to show a loading spinner. +/// +public sealed class HtmxAsyncLoad : ConditionalComponentBase +{ + [Inject] + private HtmxContext HtmxContext { get; set; } = default!; + + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "False positive. This is a parameter.")] + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary? AdditionalAttributes { get; set; } + + /// + /// The content to render out when the htmx load request arrives. + /// + [Parameter, EditorRequired] + public required RenderFragment ChildContent { get; set; } + + /// + /// The content to should be rendered while the htmx load request is in progress. + /// + [Parameter] + public RenderFragment? Loading { get; set; } + + /// + /// The ID of the element that should render it's content inside. + /// This is also the ID that htmx will use to target the element to replace it's content. + /// + [Parameter, EditorRequired] + public required string Id { get; set; } + + /// + /// The element type that should render + /// and into. + /// + /// Default is a div element. + [Parameter] + public string Element { get; set; } = "div"; + + protected override void OnParametersSet() + { + Element = string.IsNullOrWhiteSpace(Element) ? "div" : Element.Trim(); + Id = string.IsNullOrWhiteSpace(Id) ? Id : Id.Trim(); + + if (AdditionalAttributes is null) + { + return; + } + + RemoveControlledAttributeAndThrow(AdditionalAttributes, HtmxConstants.Attributes.HxGet); + RemoveControlledAttributeAndThrow(AdditionalAttributes, HtmxConstants.Attributes.HxTrigger); + RemoveControlledAttributeAndThrow(AdditionalAttributes, HtmxConstants.Attributes.HxTarget); + RemoveControlledAttributeAndThrow(AdditionalAttributes, HtmxConstants.Attributes.HxSwap); + } + + protected override void BuildRenderTree([NotNull] RenderTreeBuilder builder) + { + var request = HtmxContext.Request; + builder.OpenElement(1, Element); + builder.AddAttribute(2, HtmxConstants.Attributes.Id, Id); + if (request.RoutingMode == RoutingMode.Standard) + { + builder.AddAttribute(3, HtmxConstants.Attributes.HxGet, HtmxContext.Request.Path); + builder.AddAttribute(4, HtmxConstants.Attributes.HxTrigger, HtmxConstants.Triggers.Load); + builder.AddAttribute(5, HtmxConstants.Attributes.HxTarget, $"#{Id}"); + builder.AddAttribute(6, HtmxConstants.Attributes.HxSwap, HtmxConstants.SwapStyles.OuterHTML); + } + + if (AdditionalAttributes is not null) + { + builder.AddMultipleAttributes(7, AdditionalAttributes); + } + + if (request.RoutingMode == RoutingMode.Standard && Loading is not null) + { + builder.AddContent(8, Loading); + } + + else if (request.RoutingMode == RoutingMode.Direct || request.Target == Id && request.Trigger == Id) + { + builder.AddContent(9, ChildContent); + } + + builder.CloseElement(); + } + + private static void RemoveControlledAttributeAndThrow(IDictionary attributes, string attributeName) + { + if (attributes.Remove(attributeName)) + { + throw new ArgumentException($"The '{attributeName}' attribute is controlled by the {nameof(HtmxAsyncLoad)} components and should not be set explicitly."); + } + } +} From a5af4eeffccc948e2a0b6d9ae3d94c60309ad429 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Sun, 5 May 2024 10:09:34 +0000 Subject: [PATCH 2/2] doc: add x to blazor logo --- README.md | 6 +- docs/htmxor-icon-light-mode.png | Bin 1567 -> 0 bytes docs/htmxor-icon-square-x.png | Bin 0 -> 2453 bytes docs/htmxor-icon-square-x.svg | 97 ++++++++++++++++++++++++++++++++ docs/htmxor-icon-square.svg | 42 ++++++++++++-- docs/htmxor-x.svg | 47 ++++++++++++++++ src/Htmxor/Htmxor.csproj | 4 +- 7 files changed, 185 insertions(+), 11 deletions(-) delete mode 100644 docs/htmxor-icon-light-mode.png create mode 100644 docs/htmxor-icon-square-x.png create mode 100644 docs/htmxor-icon-square-x.svg create mode 100644 docs/htmxor-x.svg diff --git a/README.md b/README.md index 3bbbe8d..bf3acde 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Htmxor - supercharging Blazor Static SSR with Htmx -![Htmxor logo](https://raw.githubusercontent.com/egil/Htmxor/main/docs/htmxor.svg) +![Htmxor logo](https://github.com/egil/Htmxor/blob/main/docs/htmxor-x.svg) This packages enables Blazor Static SSR (.NET 8 and later) to be used seamlessly with Htmx. @@ -21,6 +21,6 @@ The following Blazor Web Apps (Htmxor) are used to test Htmxor and demo the capa ## Documentation -- **[Getting Started](/docs/getting-started.md)** - how to create a new Htmxor/Blazor project. -- **[Routing in Htmxor](/docs/routing.md)** - there are two types of routing in Htmxor, standard and direct. +- **[Getting Started](https://github.com/egil/Htmxor/blob/main/docs/getting-started.md)** - how to create a new Htmxor/Blazor project. +- **[Routing in Htmxor](https://github.com/egil/Htmxor/blob/main/docs/routing.md)** - there are two types of routing in Htmxor, standard and direct. diff --git a/docs/htmxor-icon-light-mode.png b/docs/htmxor-icon-light-mode.png deleted file mode 100644 index 6ac8f0b5c33b623f30390eb7378be1f035f8f55d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1567 zcmV+)2H^RLP)B{qr)(HJ#EvA$x| z_^6_7ceX`9qo&3f{UI6?#Sn>#F+%Z$=pSFv9~hg6X&MDV2n0nDsYtiod+#E4Jl~x= z%j~#2dw1FzuzQn}bZ=*N?|0^W&xys#tGvo<1Xr7pi7%>gjw>@!WOc@zaryRagMX8- zL`#{Wkd=&!D-sr49NbTs;y&BtPSWHX%Pi$tt<}!aNsBK5Ne7aaxHd>C_5~40Xpghg zk3<{C17ujmNPcOD!OuuqED2(^5jhEy^9O5FCwl+44RJO0(;j(Jg%-ax@HH!eD}ix` zBXK7K?Nr>tN#SVyw`*O zm)98l(ZJ8`Q)kSzt7lixZ9Jq#cr`pag|i0#UICYUq>M~3rjp-gg`$w z(b0foN0T@ccmWs)IY=mh$c~R3GLV01TZ6Mhv#pu2;G!Q#Qz>6k0)Za-l}FW$6loKI zJAiJDA19=MxAMBJ$?wXIPmUy+k^w&74TD$MCR+<)SK0=%DRE?pbQ8$mB@t+_n*&Ff z2dvT*z!2~Sa3fF|g}7?q8DOu*k1^mw;Pyf;&>9zKgBYd~z+$iHRfH=z~E~<3N`#?Q`urEymUBC^%q@sndDb~1#v2~=p z&Nmi*X{Fwy&?C1qKC4zQt_L5@sDrTw3W_2A6N|x7P0xDZgo*8R&bxv zw0ljvmRAuugf>H*;}Rj3`V3y*Zt=^B)Ev@hvD0wfxAGdqK_b4jp3U@>9ELm@ST|f1 zQ)XlWm#OSqdfR2P4yC@evM8?0)Jj5Kb-mvdbH+=T27#0`+G^7c1lmU&yDm#ioL#S_ zLQ-;_I4fezry~0@;MD?NI4lM3RZoYW(gbY_xj#om z@k~9+na1p%R3Y8ZnnIHY3i1e~HL42Z*C^FRlo+^6{op0VS|b>>N%p1&yI8qzk0L~7 zYw4E09=0HXCeM&6TWa{3Ap{B+oQ3-|zWirJW@El51ee;S z6j-f3|E~Jkr=^Sg-PDIxD`N%-l$n(V3LykOJVHt%WPF`U_xw;QoHBg?Nq?!-&T81x$%-;`Y-Yf*6W|nlo;DR zY)}qS~{0**ItNmk(W+zJL;h*MYQ~>2tkq#hX5pDoJU% zNrP`(SU)g5VscE4bn8}X_L{3|!=Zu%#>F+t*gp4N1*wX6`$*{WmmP?4Q)rlg{lBDI z;~Ar~%9%*DV$$GW1=QDN&;Ijx&GY&R+^j?}^>CR729r*Oe~Ju4cPh0!Ayo*%$b|!B z`~*e*Ac-s1=TfI)@jFXAW1o81!%7OHy}YDTkvsq6_hjQ6GOOH!vb++f1ni+H3#l|> z=s7YMWk)FHlPn3yyfU`IHEHsKz3)Ou+d@f(-hm`%I+MJv#hojLkHh|7Z_;S|Zt@=$ zDQURj{r<9}wR+$rQY97K7++mT1vVv(;46TnAs)c}pL$ZEYMLCR`Z6dfkj7nJ z#tpuOLuS4Rq$M!UdY4`%QLFZlH8>+SR+lVA47Laa{t;}$fE~d1h;!S6b3%~)8D-*F zd8x#L^22=*&eo}nJme#c{{97O{fNjO#&l)uEQ*H|;|QIWb5xm;@+vQ#^$!5c3ohd$ R4G#bS002ovPDHLkV1i87-+2H4 diff --git a/docs/htmxor-icon-square-x.png b/docs/htmxor-icon-square-x.png new file mode 100644 index 0000000000000000000000000000000000000000..e976f14deb7df06ead47150a8646e585c4042b3e GIT binary patch literal 2453 zcmbuB`#%#37{+%A$3#a+b3X~;VD8r;mzYs<7iMmATN}BIMz&O_+}2WKNg_lyQ-oL{ zskwyMgocDN+Q^Peb)3K9{P4Wr&+~jf?=SEB%aiAM+(i`uU`Xy%Lt{$zs@`+XgW zG%WSuu%cD_9;96FArpiIL>v4xKsKmu$jIuH>`{?p-?%0oZd8H(dhAE(MnW@Z`$u6L zVvX&QN<>z$ezGDd%jrJ{q6f;?tj!g#Ilbwa%U}bQ7B7In`yqQ>kHFOKFiHR0WGwGN z*~JR)6qS*nbWm~k(|{fSCNAjR30K+KL?Mp#Nls1M zh*)wc9&-bHg&aj`y_|0#zwKN>^93_V@gxYh#9_jePA_*l_6at}sQ7v&wYm za6x|L*%LEU)NrP#yrhL_D^UKDC7^q|Z4RfXljpcHXb$lWKlnEA4x|>`aT=5LD)f41 z@n{Ik2CcebZ*2XN^#zOvzSH(XRhn&@-brCP+uuOvS*D)6e>f!|iP+Te9Ddf77F9su zm_Q4QLvnv+qwlNPvON221Y5I_bD>i0VVJdkEY@&MJ4U3G2Bu%>oX`imU4~ZWTxuac zpuuy0zYgYEuuww>i0=&YVN*xyfIkDo@S=Xe`mb>#{Qc=daI~D|y#tdf``v>Z#B7R)~k=`M1vFZ~!XBFd@}{Ri^hmsOkVGVJc}YyN7rH9rNT3 zbj|ot3C_gxo2$uAo&MknD8XMb*vQ?+)~oWl`WWRAo23aJ;a@C=x#L%Xg&+cQg58WB zQi;KcqxH^NQBI(=60~55VLs*SS|6DGVAS*<0g89oxfATEO31pLxc#R9pp+eCcXCeu zIe3)Ed%nLZK6i_#XjMJ9*H?c@1JUfNux}K!M#@^KLjhUcH>R@aO4rpbQQ$P?fIW2W z*vhJp8c!wVBJq#Y7=TC?J_PAs`TADaxruDV2^z<}>h^jp<$zz|^4Y6Xxw;DXul*37 zwulsKBtj0Jar7o~S{=f{MWgylQV1=eH@S^OkcbUh)NYq<0pGM9b$i!CZ`npX_TGat zQpij$pW!}2*`@Ukn5>;^j&*EMi4lV_F6!}$ogMBHuSGD@Q)CWmWox=Ib1^Mw4m8!Lu%yMfujNlEvjWO9`p#R#lBpDBTy&{@*o8CiSB zR1mDfi?J{yxgWFfWBOb|>eeR)!JEIqI{n{O}s|b&B;G?Hk)15hTAmP zsh^F`v@w0j6-8sakyB%%(2p1HegnYDuP5sJR2f0>8b7@s*n9ni@EZ`G)X5)?0Mfi{<2rnvJ=Ahe0D$kQd;lTW;j2Z&MGZ=mfcMH zy=Zr+c_0ZJf(-gxCrz_gIp(frACXVIH>C4e2i?~9#ZLtq7`zCHtL1Gq%>V0LLz5!MOV%50tsn9tT8&WohS62nv`jAz{q-sxSX2^<*h#r + + + + + + + + + + + + + + + + + + diff --git a/docs/htmxor-icon-square.svg b/docs/htmxor-icon-square.svg index f9862db..cf7936f 100644 --- a/docs/htmxor-icon-square.svg +++ b/docs/htmxor-icon-square.svg @@ -12,7 +12,31 @@ xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + id="defs2"> + + + +