Blazor integration for FluentSignals - A reactive state management library. Includes SignalBus for component communication, HTTP resource components, typed resource factories, and Blazor-specific helpers.
$ dotnet add package FluentSignals.BlazorBlazor integration for FluentSignals - A powerful reactive state management library. This package provides Blazor-specific components, SignalBus for inter-component communication, and helpers to make working with signals in Blazor applications seamless and efficient.
dotnet add package FluentSignals.Blazor
// Program.cs
builder.Services.AddFluentSignalsBlazor(options =>
{
options.WithBaseUrl("https://api.example.com")
.WithTimeout(TimeSpan.FromSeconds(30));
});
// Or with SignalBus
builder.Services.AddFluentSignalsBlazorWithSignalBus();
A component for displaying HTTP resources with built-in loading, error, and success states. Supports dynamic request building with signal subscriptions for automatic data reloading when signals change.
<!-- Simple URL-based resource -->
<HttpResourceView T="WeatherData" Url="/api/weather">
<Success>
<WeatherDisplay Data="@context" />
</Success>
</HttpResourceView>
<!-- With custom loading and error states -->
<HttpResourceView T="User[]" Url="/api/users" @ref="userView">
<Loading>
<div class="skeleton-loader">Loading users...</div>
</Loading>
<ErrorContent>
<div class="error-panel">
<p>Failed to load users: @context.Message</p>
</div>
</ErrorContent>
<Success>
@foreach (var user in context)
{
<UserCard User="@user" />
}
</Success>
</HttpResourceView>
@code {
private TypedSignal<string> searchTerm = new("");
private TypedSignal<int> currentPage = new(1);
private TypedSignal<string> sortBy = new("name");
}
<!-- Automatically reload when signals change -->
<HttpResourceView T="PagedResult<Product>"
DynamicRequestBuilder="@BuildProductRequest"
SubscribeToSignals="@(new ISignal[] { searchTerm, currentPage, sortBy })">
<Success>
<ProductGrid Products="@context.Items" />
<Pagination TotalPages="@context.TotalPages"
CurrentPage="@currentPage.Value"
OnPageChange="@(page => currentPage.Value = page)" />
</Success>
</HttpResourceView>
@code {
private HttpRequestMessage BuildProductRequest()
{
var url = $"/api/products?search={searchTerm.Value}&page={currentPage.Value}&sort={sortBy.Value}";
return new HttpRequestMessage(HttpMethod.Get, url);
}
}
Url - The URL to fetch data from (simple GET requests)RequestBuilder - Function that builds the HTTP requestDynamicRequestBuilder - Function that builds requests using current signal valuesSubscribeToSignals - Array of signals to subscribe to for automatic reloadingLoadOnInit - Whether to load data on component initialization (default: true)ShowRetryButton - Show retry button on errors (default: true)Loading - Custom loading contentSuccess - Content to display when data is loadedEmpty - Content to display when no data is availableErrorContent - Custom error contentOnDataLoaded - Callback when data is successfully loadedOnError - Callback when an error occursOnResourceCreated - Callback when the resource is createdRefreshAsync() - Manually refresh the dataGetResource() - Get access to the underlying HttpResource@code {
private TypedSignal<string> searchTerm = new("");
private TypedSignal<int> currentPage = new(1);
private TypedSignal<int> pageSize = new(20);
}
<!-- Search bar is completely outside the component -->
<input type="text" @bind="searchTerm.Value" @bind:event="oninput"
placeholder="Search..." class="form-control mb-3" />
<HttpResourceView T="PagedResult<Product>"
DynamicRequestBuilder="@(() => new HttpRequestMessage(HttpMethod.Get,
$"/api/products?q={searchTerm.Value}&page={currentPage.Value}&size={pageSize.Value}"))"
SubscribeToSignals="@(new[] { searchTerm, currentPage, pageSize })">
<Success>
<!-- Custom rendering of results -->
@foreach (var product in context.Items)
{
<ProductCard Item="@product" />
}
<!-- Custom pagination controls -->
<Pagination CurrentPage="@currentPage.Value"
TotalPages="@context.TotalPages"
OnPageChange="@(page => currentPage.Value = page)" />
</Success>
</HttpResourceView>
@code {
private TypedSignal<string?> nextCursor = new(null);
private List<Post> allPosts = new();
}
<HttpResourceView T="CursorResult<Post>"
DynamicRequestBuilder="@(() => new HttpRequestMessage(HttpMethod.Get,
$"/api/posts?cursor={nextCursor.Value ?? ""}"))"
SubscribeToSignals="@(new[] { nextCursor })"
OnDataLoaded="@(result => { allPosts.AddRange(result.Items); })">
<Success>
<div class="posts-container">
@foreach (var post in allPosts)
{
<PostItem Data="@post" />
}
@if (context.HasMore)
{
<button @onclick="() => nextCursor.Value = context.NextCursor">
Load More
</button>
}
</div>
</Success>
</HttpResourceView>
@code {
private TypedSignal<string> category = new("");
private TypedSignal<decimal?> minPrice = new(null);
private TypedSignal<decimal?> maxPrice = new(null);
private TypedSignal<bool> inStock = new(false);
private HttpRequestMessage BuildFilterRequest()
{
var filters = new {
Category = category.Value,
PriceRange = new { Min = minPrice.Value, Max = maxPrice.Value },
InStockOnly = inStock.Value
};
var request = new HttpRequestMessage(HttpMethod.Post, "/api/products/search");
request.Content = JsonContent.Create(filters);
return request;
}
}
<!-- Filter controls are separate from the component -->
<div class="filters">
<select @bind="category.Value">
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<input type="number" @bind="minPrice.Value" placeholder="Min Price" />
<input type="number" @bind="maxPrice.Value" placeholder="Max Price" />
<label>
<input type="checkbox" @bind="inStock.Value" />
In Stock Only
</label>
</div>
<HttpResourceView T="SearchResult<Product>"
DynamicRequestBuilder="@BuildFilterRequest"
SubscribeToSignals="@(new[] { category, minPrice, maxPrice, inStock })">
<Success>
<ProductResults Results="@context" />
</Success>
</HttpResourceView>
The key advantage is that the HttpResourceView component doesn't care about how you build your UI or structure your requests. It simply:
This separation allows complete flexibility in UI design while maintaining reactive data fetching.
A generic component for displaying any async resource with automatic state management.
A specialized component for SignalR real-time data with connection status display.
A base component class that provides automatic signal integration and lifecycle management.
Create strongly-typed HTTP resource classes with automatic dependency injection:
// Define your resource with the HttpResource attribute
[HttpResource("/api/users")]
public class UserResource : TypedHttpResource
{
// Parameterless constructor required for factory
public UserResource() { }
public HttpResourceRequest<User> GetById(int id) =>
Get<User>($"{BaseUrl}/{id}");
public HttpResourceRequest<IEnumerable<User>> GetAll() =>
Get<IEnumerable<User>>(BaseUrl);
public HttpResourceRequest<User> Create(User user) =>
Post<User>(BaseUrl, user);
public HttpResourceRequest<User> Update(int id, User user) =>
Put<User>($"{BaseUrl}/{id}", user);
public HttpResourceRequest Delete(int id) =>
Delete($"{BaseUrl}/{id}");
}
Register and use typed resources with factory:
// Registration in Program.cs
services.AddFluentSignalsBlazor(options =>
{
options.BaseUrl = "https://api.example.com";
});
services.AddTypedHttpResourceFactory<UserResource>();
// Usage in components
@inject ITypedHttpResourceFactory<UserResource> UserFactory
@code {
private UserResource? users;
private HttpResource? userResource;
protected override async Task OnInitializedAsync()
{
// Create resource with DI-configured HttpClient
users = UserFactory.Create();
// Or create with custom options
users = UserFactory.Create(options =>
{
options.Timeout = TimeSpan.FromSeconds(60);
});
// Execute requests
userResource = await users.GetById(123).ExecuteAsync();
userResource.OnSuccess(() => ShowNotification("User loaded!"));
}
}
Alternatively, inject the resource directly:
@inject UserResource Users
@code {
protected override async Task OnInitializedAsync()
{
var resource = await Users.GetById(123).ExecuteAsync();
resource.OnSuccess(() => ShowNotification("User loaded!"));
}
}
Create fully typed custom methods for complex scenarios:
[HttpResource("/api/v2")]
public class AdvancedApiResource : TypedHttpResource
{
public AdvancedApiResource() { }
// Typed search with complex criteria
public HttpResourceRequest<SearchResult<Product>> SearchProducts(ProductSearchCriteria criteria)
{
return Post<ProductSearchCriteria, SearchResult<Product>>($"{BaseUrl}/products/search", criteria)
.WithHeader("X-Search-Version", "2.0")
.ConfigureResource(r =>
{
r.OnSuccess(result => Console.WriteLine($"Found {result.Data.TotalCount} products"));
r.OnNotFound(() => Console.WriteLine("No products found"));
});
}
// Batch operations with progress tracking
public HttpResourceRequest<BatchResult> ProcessBatch(BatchRequest batch)
{
return Post<BatchRequest, BatchResult>($"{BaseUrl}/batch", batch)
.WithHeader("X-Batch-Id", Guid.NewGuid().ToString())
.ConfigureResource(r =>
{
r.IsLoading.Subscribe(loading =>
{
if (loading) ShowProgress("Processing batch...");
else HideProgress();
});
});
}
// File upload with typed metadata
public HttpResourceRequest<UploadResult> UploadFile(Stream file, FileMetadata metadata)
{
return BuildRequest<UploadResult>($"{BaseUrl}/files")
.WithMethod(HttpMethod.Post)
.WithBody(new { file, metadata })
.WithHeader("Content-Type", "multipart/form-data")
.WithQueryParam("category", metadata.Category)
.Build()
.ConfigureResource(r => r.OnServerError(() => ShowError("Upload failed")));
}
}
// Usage in component
@inject ITypedHttpResourceFactory<AdvancedApiResource> ApiFactory
@code {
private async Task SearchProducts()
{
var api = ApiFactory.Create();
var criteria = new ProductSearchCriteria
{
Query = searchText,
MinPrice = 10,
MaxPrice = 100
};
var resource = await api.SearchProducts(criteria).ExecuteAsync();
// Resource will handle success/error states automatically
}
}
The SignalBus provides a publish/subscribe pattern for component communication with support for both standard and queue-based subscriptions.
For detailed documentation and examples, visit our GitHub repository.
This project is licensed under the MIT License.