diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 9630b16..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "nuget" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "daily" - open-pull-requests-limit: 25 \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5bc8d67..0328098 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,42 +29,54 @@ defaults: permissions: contents: read +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: -# run_test: -# runs-on: ubuntu-latest -# steps: -# - name: Checkout Source Code -# uses: actions/checkout@v3 -# -# - name: Setup .NET SDK -# uses: actions/setup-dotnet@v3 -# with: -# dotnet-version: 8.x -# -# - name: Run Unit Tests -# run: dotnet test --configuration Release --verbosity normal --collect:"XPlat Code Coverage" -# -# - name: Upload Code Coverage -# uses: codecov/codecov-action@v3 - + run_test: + runs-on: ubuntu-latest + steps: + - name: Checkout Source Code + uses: actions/checkout@v6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.x + + - name: Run Unit Tests + run: dotnet test --configuration Release --verbosity normal --collect:"XPlat Code Coverage" + + - name: Upload Code Coverage + uses: codecov/codecov-action@v5 + create_nuget: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 # Get all history to allow automatic versioning using MinVer # Install the .NET SDK indicated in the global.json file - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 8.x + dotnet-version: 10.x + - name: Restore dependencies + run: dotnet restore + + # Build the project first so that staticwebassets.build.json manifest is generated + - name: Build + run: dotnet build --configuration Release --no-restore + # Create the NuGet package in the folder from the environment variable NuGetDirectory - - run: dotnet pack --configuration Release --output ${{ env.NuGetDirectory }} + - name: Create NuGet package + run: dotnet pack --configuration Release --no-build --output ${{ env.NuGetDirectory }} # Publish the NuGet package as an artifact, so they can be used in the following jobs - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v7 with: name: nuget if-no-files-found: error @@ -77,12 +89,12 @@ jobs: steps: # Install the .NET SDK indicated in the global.json file - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 8.x + dotnet-version: 10.x # Download the NuGet package created in the previous job - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v8 with: name: nuget path: ${{ env.NuGetDirectory }} @@ -104,7 +116,7 @@ jobs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -113,20 +125,19 @@ jobs: # https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository # You can update this logic if you want to manage releases differently runs-on: ubuntu-latest - needs: [ validate_nuget, update_release_draft ] -# needs: [ validate_nuget, run_test, update_release_draft ] + needs: [ validate_nuget, run_test, update_release_draft ] steps: # Download the NuGet package created in the previous job - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v8 with: name: nuget path: ${{ env.NuGetDirectory }} # Install the .NET SDK indicated in the global.json file - name: Setup .NET Core - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 8.x + dotnet-version: 10.x # Publish all NuGet packages to NuGet.org # Use --skip-duplicate to prevent errors if a package with the same version already exists. @@ -134,5 +145,5 @@ jobs: - name: Publish NuGet package run: | foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) { - dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push $file --api-key "${{ secrets.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate } \ No newline at end of file diff --git a/README.md b/README.md index 9883581..40a2cce 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,241 @@ # FastComponents -Code web, think .NET 8 with FastComponents for a successful MRA (Multiple Resources Application). +> **Server-side Blazor components rendered as HTMX-powered HTML fragments -- build interactive web UIs with .NET 10 and zero client-side Blazor runtime.** + + +[![Atypical-Consulting - FastComponents](https://img.shields.io/static/v1?label=Atypical-Consulting&message=FastComponents&color=blue&logo=github)](https://github.com/Atypical-Consulting/FastComponents) +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) +[![.NET 10](https://img.shields.io/badge/.NET-10.0-purple?logo=dotnet)](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) +[![stars - FastComponents](https://img.shields.io/github/stars/Atypical-Consulting/FastComponents?style=social)](https://github.com/Atypical-Consulting/FastComponents) +[![forks - FastComponents](https://img.shields.io/github/forks/Atypical-Consulting/FastComponents?style=social)](https://github.com/Atypical-Consulting/FastComponents) + + +[![GitHub tag](https://img.shields.io/github/tag/Atypical-Consulting/FastComponents?include_prereleases=&sort=semver&color=blue)](https://github.com/Atypical-Consulting/FastComponents/releases/) +[![issues - FastComponents](https://img.shields.io/github/issues/Atypical-Consulting/FastComponents)](https://github.com/Atypical-Consulting/FastComponents/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/Atypical-Consulting/FastComponents)](https://github.com/Atypical-Consulting/FastComponents/pulls) +[![GitHub last commit](https://img.shields.io/github/last-commit/Atypical-Consulting/FastComponents)](https://github.com/Atypical-Consulting/FastComponents/commits/main) + + +[![Build](https://github.com/Atypical-Consulting/FastComponents/actions/workflows/main.yml/badge.svg)](https://github.com/Atypical-Consulting/FastComponents/actions/workflows/main.yml) + + +[![NuGet](https://img.shields.io/nuget/v/FastComponents.svg)](https://www.nuget.org/packages/FastComponents) + +--- + +## Table of Contents + +- [The Problem](#the-problem) +- [The Solution](#the-solution) +- [Features](#features) +- [Tech Stack](#tech-stack) +- [Getting Started](#getting-started) +- [Usage](#usage) +- [Architecture](#architecture) +- [Project Structure](#project-structure) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) + +## The Problem + +Building interactive web UIs with .NET typically means choosing between the full Blazor Server runtime (with WebSocket overhead and connection state) or Blazor WASM (with large download sizes). If you just want lightweight, server-rendered HTML fragments that respond to user interactions -- the HTMX model -- there is no first-class Blazor integration. You end up writing raw HTML strings or abandoning the Razor component model entirely. + +## The Solution + +**FastComponents** bridges Blazor's server-side Razor component model with HTMX. You author components using familiar `.razor` syntax with full C# support, and FastComponents renders them as plain HTML fragments served via FastEndpoints. HTMX on the client handles partial page updates -- no WebSocket connections, no WASM downloads, just HTTP requests and HTML responses. + +```csharp +// Define a component with HTMX attributes using the HtmxTag helper + + Increment + +``` + +## Features + +- [x] `HtmxComponentBase` -- base class with all htmx attributes as Blazor `[Parameter]` properties +- [x] `HtmxTag` -- generic Razor component that renders any HTML element with htmx attributes +- [x] `HtmxComponentEndpoint` -- serve components as HTML via FastEndpoints routes +- [x] `HtmxComponentEndpoint` -- typed parameter binding from query strings +- [x] `HtmxComponentParameters` -- immutable record base with automatic query string serialization +- [x] `ComponentHtmlResponseService` -- render any Blazor component to an HTML string on the server +- [x] `ClassNamesBuilder` -- fluent, conditional CSS class builder (similar to `classnames` in JS) +- [x] `Hx.Swap` constants and `Hx.TargetId()` helper for type-safe htmx attribute values +- [x] Bundled `htmx.min.js` as a static web asset +- [x] NuGet package with auto-generated API documentation +- [ ] HTML beautifier for formatted output *(stub implemented)* +- [ ] AOT compilation support *(planned)* +- [ ] Project template for `dotnet new` *(planned)* + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Runtime | .NET 10.0 / C# 14 | +| Component model | Blazor SSR (`Microsoft.AspNetCore.Components.Web` 10.0) | +| Endpoint routing | [FastEndpoints](https://fast-endpoints.com/) 8.0 | +| HTML parsing | [AngleSharp](https://anglesharp.github.io/) 1.4 | +| Client interactivity | [htmx](https://htmx.org/) (bundled) | +| Versioning | [MinVer](https://github.com/adamralph/minver) (git-tag based) | +| CI/CD | GitHub Actions (build, NuGet pack, validate, publish) | + +## Getting Started + +### Prerequisites + +- [.NET SDK](https://dotnet.microsoft.com/download) >= 10.0 + +### Installation + +**Option 1 -- NuGet** *(recommended)* + +```bash +dotnet add package FastComponents +``` + +**Option 2 -- From Source** + +```bash +git clone https://github.com/Atypical-Consulting/FastComponents.git +cd FastComponents +dotnet build +``` + +### Setup + +Register FastComponents in your `Program.cs`: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Add FastComponents services (registers FastEndpoints + HTML renderer) +builder.Services.AddFastComponents(); + +var app = builder.Build(); + +app.UseStaticFiles(); + +// Map component endpoints +app.UseFastComponents(); + +app.Run(); +``` + +## Usage + +### Define a component + +Create a Razor component that inherits from `HtmxComponentBase`: + +```razor +@* Counter.razor *@ +@inherits HtmxComponentBase + +
+ + Increment + + + Count: @Parameters.Count +
+``` + +### Wire it to an endpoint + +```csharp +// Counter.razor.cs +public class CounterEndpoint + : HtmxComponentEndpoint +{ + public override void Configure() + { + Get("/ui/blocks/counter"); + AllowAnonymous(); + } + + public record CounterParameters : HtmxComponentParameters + { + public int Count { get; init; } = 0; + + public string Increment() + { + var next = this with { Count = Count + 1 }; + return next.ToComponentUrl("/ui/blocks/counter"); + } + } +} +``` + +When the button is clicked, htmx sends a GET request to `/ui/blocks/counter?Count=1`, FastComponents renders the Blazor component server-side, and the HTML fragment replaces the target element -- no JavaScript framework required. + +## Architecture + +``` +Browser (htmx) ASP.NET Server ++-----------------+ +-----------------------------------+ +| HTML + htmx.js | -- HTTP GET -> | FastEndpoints route | +| | | -> HtmxComponentEndpoint | +| | | -> ComponentHtmlResponseService| +| | | -> HtmlRenderer (Blazor SSR)| +| <-- HTML frag -- | | <- Rendered HTML fragment | ++-----------------+ +-----------------------------------+ +``` + +### Project Structure + +``` +FastComponents/ +├── src/ +│ └── FastComponents/ # Core library (NuGet package) +│ ├── Components/ +│ │ ├── Base/ # HtmxComponentBase, ClassNamesBuilder, interfaces +│ │ └── HtmxTag/ # Generic htmx-aware Razor component +│ ├── Endpoints/ # HtmxComponentEndpoint base classes +│ ├── Services/ # ComponentHtmlResponseService, HtmlBeautifier +│ ├── Utilities/ # Hx helper (swap constants, target helpers) +│ └── wwwroot/ # Bundled htmx.min.js +├── demo/ +│ └── HtmxAppServer/ # Demo app (Counter, MovieCharacters examples) +├── docs/ # Auto-generated API documentation +├── build/ # Build scripts (versioning) +└── .github/workflows/ # CI pipeline (build, pack, validate, deploy) +``` + +## Roadmap + +- [ ] Implement HTML beautifier for formatted debug output +- [ ] Add AOT compilation support +- [ ] Publish a `dotnet new` project template +- [ ] Add unit and integration tests +- [ ] Expand component library with common UI patterns + +> Want to contribute? Pick any roadmap item and open a PR! + +## Contributing + +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) first. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License + +[Apache-2.0](LICENSE) (c) 2020-2026 [Atypical Consulting](https://atypical.garry-ai.cloud) + +--- + +Built with care by [Atypical Consulting](https://atypical.garry-ai.cloud) -- opinionated, production-grade open source. + +[![Contributors](https://contrib.rocks/image?repo=Atypical-Consulting/FastComponents)](https://github.com/Atypical-Consulting/FastComponents/graphs/contributors) diff --git a/demo/HtmxAppServer/Dockerfile b/demo/HtmxAppServer/Dockerfile index 1ae8d32..56b457c 100644 --- a/demo/HtmxAppServer/Dockerfile +++ b/demo/HtmxAppServer/Dockerfile @@ -1,10 +1,10 @@ -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base USER $APP_UID WORKDIR /app EXPOSE 8080 EXPOSE 8081 -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["HtmxAppServer/HtmxAppServer.csproj", "HtmxAppServer/"] diff --git a/docs/fast-components/FastComponents.ComponentHtmlResponseService.RenderComponent.md b/docs/fast-components/FastComponents.ComponentHtmlResponseService.RenderComponent.md new file mode 100644 index 0000000..dc68243 --- /dev/null +++ b/docs/fast-components/FastComponents.ComponentHtmlResponseService.RenderComponent.md @@ -0,0 +1,68 @@ +#### [FastComponents](FastComponents.md 'FastComponents') +### [FastComponents](FastComponents.md 'FastComponents').[ComponentHtmlResponseService](FastComponents.ComponentHtmlResponseService.md 'FastComponents\.ComponentHtmlResponseService') + +## ComponentHtmlResponseService\.RenderComponent Method + +| Overloads | | +| :--- | :--- | +| [RenderComponent<TComponent>\(ParameterView\)](FastComponents.ComponentHtmlResponseService.RenderComponent.md#FastComponents.ComponentHtmlResponseService.RenderComponent_TComponent_(Microsoft.AspNetCore.Components.ParameterView) 'FastComponents\.ComponentHtmlResponseService\.RenderComponent\\(Microsoft\.AspNetCore\.Components\.ParameterView\)') | Use the default dispatcher to invoke actions in the context of the static HTML renderer and return as a string | +| [RenderComponent<TComponent>\(Dictionary<string,object>\)](FastComponents.ComponentHtmlResponseService.RenderComponent.md#FastComponents.ComponentHtmlResponseService.RenderComponent_TComponent_(System.Collections.Generic.Dictionary_string,object_) 'FastComponents\.ComponentHtmlResponseService\.RenderComponent\\(System\.Collections\.Generic\.Dictionary\\)') | Renders a component T | + + + +## ComponentHtmlResponseService\.RenderComponent\\(ParameterView\) Method + +Use the default dispatcher to invoke actions in the context of the +static HTML renderer and return as a string + +```csharp +private System.Threading.Tasks.Task RenderComponent(Microsoft.AspNetCore.Components.ParameterView parameters) + where TComponent : FastComponents.HtmxComponentBase; +``` +#### Type parameters + + + +`TComponent` + +The component to render +#### Parameters + + + +`parameters` [Microsoft\.AspNetCore\.Components\.ParameterView](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview 'Microsoft\.AspNetCore\.Components\.ParameterView') + +The parameters to pass to the component + +#### Returns +[System\.Threading\.Tasks\.Task<](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')[System\.String](https://learn.microsoft.com/en-us/dotnet/api/system.string 'System\.String')[>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1') +The rendered component as a string + + + +## ComponentHtmlResponseService\.RenderComponent\\(Dictionary\\) Method + +Renders a component T + +```csharp +public System.Threading.Tasks.Task RenderComponent(System.Collections.Generic.Dictionary? dictionary=null) + where TComponent : FastComponents.HtmxComponentBase; +``` +#### Type parameters + + + +`TComponent` + +The component to render +#### Parameters + + + +`dictionary` [System\.Collections\.Generic\.Dictionary<](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2 'System\.Collections\.Generic\.Dictionary\`2')[System\.String](https://learn.microsoft.com/en-us/dotnet/api/system.string 'System\.String')[,](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2 'System\.Collections\.Generic\.Dictionary\`2')[System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object')[>](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2 'System\.Collections\.Generic\.Dictionary\`2') + +The dictionary of parameters to pass to the component + +#### Returns +[System\.Threading\.Tasks\.Task<](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')[System\.String](https://learn.microsoft.com/en-us/dotnet/api/system.string 'System\.String')[>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1') +The rendered component as a string \ No newline at end of file diff --git a/renovate.json b/renovate.json index 5db72dd..a18607a 100644 --- a/renovate.json +++ b/renovate.json @@ -2,5 +2,50 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" - ] + ], + "baseBranchPatterns": [ + "dev" + ], + "schedule": [ + "before 9am on Saturday" + ], + "timezone": "Europe/Brussels", + "labels": [ + "dependencies", + "renovate" + ], + "packageRules": [ + { + "description": "Automerge minor and patch updates", + "matchUpdateTypes": [ + "minor", + "patch" + ], + "automerge": true, + "automergeType": "pr" + }, + { + "description": "Group all NuGet minor/patch updates together", + "matchManagers": [ + "nuget" + ], + "matchUpdateTypes": [ + "minor", + "patch" + ], + "groupName": "NuGet minor/patch updates" + }, + { + "description": "Group all NuGet major updates together", + "matchManagers": [ + "nuget" + ], + "matchUpdateTypes": [ + "major" + ], + "groupName": "NuGet major updates", + "dependencyDashboardApproval": true + } + ], + "platformAutomerge": true }