Skip to content

phmatray/FormCraft

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

311 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

FormCraft 🎨

NuGet Version NuGet Downloads MudBlazor Version Build Status License Stars

Build type-safe, dynamic forms in Blazor with ease ✨

Get Started β€’ Live Demo β€’ Documentation β€’ Examples β€’ Contributing


🌐 Live Demo

Experience FormCraft in action! Visit our interactive demo to see:

  • 🎯 Various form layouts and configurations
  • πŸ”„ Dynamic field dependencies
  • ✨ Custom field renderers
  • πŸ“€ File upload capabilities
  • 🎨 Real-time form generation

πŸŽ‰ What's New in v3.1.0

v3.1.0 implements every issue that was open after v3.0 β€” all features, no breaking changes. Full changelog β†’

  • Zero-config forms β€” AddFieldsAuto() generates a complete form from any POCO by reflection: humanized labels, sensible field types per property type, DataAnnotations honored when present, none required (#124)
  • Security enforcement β€” WithSecurity() is now enforced automatically by FormCraftComponent: rate limiting (with SecurityContextId parameter), CSRF validation, and FormSubmitted/FormRejected audit entries with redaction; plus EncryptConfiguredFields() for one-call persistence encryption (#147)
  • Configurable MudBlazor Variant β€” .WithVariant(Variant.Filled) per field and a DefaultVariant parameter on FormCraftComponent, honored by every input component (#146)
  • Async field dependencies β€” DependsOn(x => x.Country, async (model, country) => ...) is a first-class overload; cascades re-render automatically when the async work settles (#93)
  • Nullable value types round-trip β€” int?/decimal?/DateTime?/DateOnly?/TimeOnly? fields display empty when null and write null back when cleared, instead of being coerced to 0/MinValue (#150)
  • Native nested validation for collections β€” collection item edits raise Items[0].ProductName field identifiers on the EditContext, so ValidationSummary/IsModified work for child rows (#91)
  • Single render pipeline β€” the legacy type-switch is gone; every field flows through FieldRendererService, and AsMultiSelect fields (previously skipped silently) now render a real multi-selection select (#148)
  • Master-detail & auto-form demos β€” new /master-detail (invoice + LOV customer + line items + computed totals) and /auto-form pages (#130)
  • Polish β€” single-file uploads no longer emit a stray multiple attribute (#149), Related Demos show real titles (#152), WithAutocomplete() + correct password autocomplete tokens (#153), validator mutations through the object-typed wrapper now take effect via AddValidator (#151)

πŸŽ‰ What's New in v3.0.0

v3.0.0 is a major quality release: after a full audit of every subsystem, 60+ bugs were fixed, several long-broken features now actually work, and the whole demo site was verified end-to-end in a real browser. Full changelog β†’

βœ… Now working as documented

  • Field dependencies β€” DependsOn(x => x.Country, ...) callbacks now fire when the watched field changes, dependent fields refresh in the UI, and async callbacks drive cascading loads (country β†’ state β†’ city)
  • Async validation blocks submission β€” OnValidSubmit waits for async validators; errors clear as soon as the user corrects a value; hidden fields are no longer validated
  • Custom rendering β€” WithCustomTemplate() renders, WithCustomRenderer(instance) is honored, and LOV/lookup/autocomplete/select renderers are no longer shadowed by the generic text/numeric ones
  • More field types β€” DateOnly, TimeOnly, float, long, short, and byte fields render correctly
  • Form templates β€” FormTemplates.ContactForm/RegistrationForm/LoginForm/AddressForm<T>() generate real convention-based forms
  • New API β€” FormCraftComponent.ValidateAsync() for explicit validation (e.g. in dialogs)

πŸ”’ Security hardening

  • Default IEncryptionService is now AES-256 (DefaultEncryptionService) with a random IV per operation; decryption failures throw FormCraftDecryptionException instead of returning ciphertext
  • Thread-safe rate limiting, CSRF tokens that survive prerendering, and audit logs that honor ExcludedFields redaction

⚠️ Breaking changes (migration notes)

Change What to do
FieldDependencies is keyed by the watched field's name Only affects code inspecting the configuration dictionary directly
DependsOn callbacks now fire on watched-field changes Remove any workarounds written for the old inverted behavior
FormBuilder throws if mutated after Build() Create a new builder instead of reusing one
WithFluentValidation fails when no IValidator<TModel> is registered Register your validator in DI (it silently passed before)
Validator exceptions surface as "Validation could not be completed: …" Don't rely on crashes producing the configured error message
AsFileUpload/AsMultipleFileUpload no longer force a renderer No action β€” the proper upload components are picked by field type
Default encryption switched from XOR to AES-256 Configure a 32-byte key (Base64 or UTF-8); without one an ephemeral per-process key is used
FieldGroupBuilder.WithColumns validates its range (1–6) Fix any out-of-range values (0 used to crash rendering)
FormCraft.ForMudBlazor now versions with the core package Reference 3.0.0 for both packages

πŸš€ Why FormCraft?

FormCraft revolutionizes form building in Blazor applications by providing a fluent, type-safe API that makes complex forms simple. Say goodbye to repetitive form markup and hello to elegant, maintainable code.

✨ Key Features

  • πŸ”’ Type-Safe - Full IntelliSense support with compile-time validation
  • 🎯 Fluent API - Intuitive method chaining for readable form configuration
  • 🏷️ Attribute-Based Forms - Generate forms from model attributes with zero configuration
  • 🎨 MudBlazor Integration - Beautiful Material Design components out of the box
  • πŸ”„ Dynamic Forms - Create forms that adapt based on user input
  • βœ… Advanced Validation - Built-in, custom, and async validators
  • πŸ”— Field Dependencies - Link fields together with reactive updates
  • πŸ“ Flexible Layouts - Multiple layout options to fit your design
  • πŸš€ High Performance - Optimized rendering with minimal overhead
  • πŸ§ͺ Fully Tested - 880+ unit tests ensuring reliability

πŸ“Š How FormCraft Compares

FormCraft stands out among Blazor form solutions with its type-safe fluent API, automatic field rendering, and built-in field dependency management. See how it compares to Blazor EditForm, Blazored.FluentValidation, and MudBlazor Forms:

Capability EditForm MudBlazor Forms FormCraft
Fluent API configuration - - Yes
Automatic field rendering - - Yes
Built-in field dependencies Manual Manual Yes
Conditional visibility Manual Manual Built-in
Field-level encryption - - Yes
Attribute-based generation - - Yes

View the full comparison β€” includes detailed feature matrix, code examples, and guidance on when to use each solution.

πŸ“¦ Installation

FormCraft Core

dotnet add package FormCraft

FormCraft for MudBlazor

dotnet add package FormCraft.ForMudBlazor

Note: FormCraft.ForMudBlazor includes FormCraft as a dependency, so you only need to install the MudBlazor package if you're using MudBlazor components.

Supported frameworks: .NET 8, .NET 9, and .NET 10.

🎯 Quick Start

1. Register Services

// Program.cs
builder.Services.AddMudServices();          // MudBlazor services
builder.Services.AddFormCraft();            // FormCraft core services
builder.Services.AddFormCraftMudBlazor();   // MudBlazor renderers for FormCraft

2. Create Your Model

public class UserRegistration
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }
    public string Country { get; set; }
    public bool AcceptTerms { get; set; }
}

3. Build Your Form

@page "/register"
@using FormCraft
@using FormCraft.ForMudBlazor

<h3>User Registration</h3>

<FormCraftComponent TModel="UserRegistration" 
                   Model="@model" 
                   Configuration="@formConfig"
                   OnValidSubmit="@HandleSubmit"
                   ShowSubmitButton="true" />

@code {
    private UserRegistration model = new();
    private IFormConfiguration<UserRegistration> formConfig;

    protected override void OnInitialized()
    {
        formConfig = FormBuilder<UserRegistration>.Create()
            .AddRequiredTextField(x => x.FirstName, "First Name")
            .AddRequiredTextField(x => x.LastName, "Last Name")
            .AddEmailField(x => x.Email)
            .AddNumericField(x => x.Age, "Age", min: 18, max: 120)
            .AddDropdownField(x => x.Country, "Country",
                ("us", "United States"),
                ("uk", "United Kingdom"),
                ("ca", "Canada"),
                ("au", "Australia"))
            .AddField(x => x.AcceptTerms, field => field
                .WithLabel("I accept the terms and conditions")
                .Required("You must accept the terms"))
            .Build();
    }

    private async Task HandleSubmit(UserRegistration model)
    {
        // Handle form submission
        await UserService.RegisterAsync(model);
    }
}

🏷️ Attribute-Based Forms (NEW!)

Define your forms directly on your model with attributes - no configuration code needed!

Define Your Model with Attributes

public class UserRegistration
{
    [TextField("First Name", "Enter your first name")]
    [Required(ErrorMessage = "First name is required")]
    [MinLength(2)]
    public string FirstName { get; set; } = string.Empty;
    
    [TextField("Last Name", "Enter your last name")]
    [Required(ErrorMessage = "Last name is required")]
    public string LastName { get; set; } = string.Empty;
    
    [EmailField("Email Address")]
    [Required]
    public string Email { get; set; } = string.Empty;
    
    [NumberField("Age", "Your age")]
    [Range(18, 120, ErrorMessage = "Age must be between 18 and 120")]
    public int Age { get; set; }
    
    [DateField("Date of Birth")]
    public DateTime BirthDate { get; set; }
    
    [SelectField("Country", "United States", "Canada", "United Kingdom", "Australia")]
    public string Country { get; set; } = string.Empty;
    
    [TextArea("Bio", "Tell us about yourself")]
    [MaxLength(500)]
    public string Bio { get; set; } = string.Empty;
    
    [CheckboxField("Newsletter", "Subscribe to our newsletter")]
    public bool SubscribeToNewsletter { get; set; }
}

Generate the Form with One Line

var formConfig = FormBuilder<UserRegistration>.Create()
    .AddFieldsFromAttributes()  // That's it! πŸŽ‰
    .Build();

Available Attribute Types

  • [TextField] - Standard text input
  • [EmailField] - Email input with validation
  • [NumberField] - Numeric input with min/max support
  • [DateField] - Date picker with constraints
  • [SelectField] - Dropdown with predefined options
  • [CheckboxField] - Boolean checkbox
  • [TextArea] - Multiline text input

All attributes work seamlessly with standard DataAnnotations validators like [Required], [MinLength], [MaxLength], [Range], and more!

Comparison: Fluent API vs Attributes

Fluent API Attribute-Based
var config = FormBuilder<User>.Create()
    .AddField(x => x.Name, field => field
        .WithLabel("Full Name")
        .WithPlaceholder("Enter name")
        .Required("Name is required")
        .WithMinLength(2))
    .AddField(x => x.Email, field => field
        .WithLabel("Email")
        .WithInputType("email")
        .Required())
    .Build();
public class User
{
    [TextField("Full Name", "Enter name")]
    [Required(ErrorMessage = "Name is required")]
    [MinLength(2)]
    public string Name { get; set; }
    
    [EmailField("Email")]
    [Required]
    public string Email { get; set; }
}

// One line to generate!
var config = FormBuilder<User>.Create()
    .AddFieldsFromAttributes()
    .Build();

🎨 Examples

Dynamic Field Dependencies

Create forms where fields react to each other. DependsOn(watchedField, callback) runs the callback whenever the watched field changes, letting you reset or recalculate dependent values:

var formConfig = FormBuilder<OrderForm>.Create()
    .AddDropdownField(x => x.ProductType, "Product Type",
        ("standard", "Standard"),
        ("premium", "Premium"))
    .AddField(x => x.ProductModel, field => field
        .WithLabel("Model")
        .WithOptions(
            ("basic", "Basic Model"),
            ("pro", "Pro Model"))
        // Reset the model whenever Product Type changes
        .DependsOn(x => x.ProductType, (model, productType) =>
            model.ProductModel = string.Empty))
    .AddNumericField(x => x.Quantity, "Quantity", min: 1)
    .AddField(x => x.TotalPrice, field => field
        .WithLabel("Total Price")
        .ReadOnly()
        // Recalculate the total whenever Quantity changes
        .DependsOn(x => x.Quantity, (model, quantity) =>
            model.TotalPrice = quantity * GetUnitPrice(model.ProductModel)))
    .Build();

Custom Validation

Add complex validation logic with ease:

.AddField(x => x.Username, field => field
    .WithValidator(
        username => !forbiddenUsernames.Contains(username.ToLower()),
        "This username is not available")
    .WithAsyncValidator(
        async username => await UserService.IsUsernameAvailableAsync(username),
        "Username is already taken"))

If a validator needs access to other model values or DI services, implement IFieldValidator<TModel, TValue> β€” its ValidateAsync(model, value, services) method receives the full model and the IServiceProvider:

public class UniqueUsernameValidator : IFieldValidator<User, string>
{
    public string? ErrorMessage { get; set; } = "Username is already taken";

    public async Task<ValidationResult> ValidateAsync(
        User model, string value, IServiceProvider services)
    {
        var userService = services.GetRequiredService<IUserService>();
        return await userService.IsUsernameAvailableAsync(value)
            ? ValidationResult.Success()
            : ValidationResult.Failure("Username is already taken");
    }
}

// Usage
.AddField(x => x.Username, field => field
    .WithValidator(new UniqueUsernameValidator()))

Multiple Layouts

Choose the layout that fits your design:

// Vertical Layout (default)
.WithLayout(FormLayout.Vertical)

// Horizontal Layout
.WithLayout(FormLayout.Horizontal)

// Grid Layout
.WithLayout(FormLayout.Grid)

// Inline Layout
.WithLayout(FormLayout.Inline)

Column counts are configured per field group rather than at the form level:

.AddFieldGroup(group => group
    .WithGroupName("Address")
    .WithColumns(2)  // Two-column layout for this group
    .AddField(x => x.City)
    .AddField(x => x.PostalCode))

Advanced Field Types

// Password field with strength requirements
.AddPasswordField(x => x.Password, "Password", minLength: 8, requireSpecialChars: true)

// Password confirmation via a model-aware validator
.AddField(x => x.ConfirmPassword, field => field
    .WithLabel("Confirm Password")
    .WithInputType("password")
    .Required("Please confirm your password")
    .WithValidator(new PasswordsMatchValidator()))

// Date picker with validation (DateTime properties render as date pickers automatically)
.AddField(x => x.BirthDate, field => field
    .WithLabel("Date of Birth")
    .WithValidator(date => date <= DateTime.Today.AddYears(-18), "Must be 18 or older")
    .WithHelpText("Must be 18 or older"))

// Multi-line text with character limit
.AddField(x => x.Description, field => field
    .WithLabel("Description")
    .AsTextArea(lines: 5, maxLength: 500)
    .WithMaxLength(500, "Maximum 500 characters")
    .WithHelpText("Maximum 500 characters"))

// File upload
.AddFileUploadField(x => x.Resume, "Upload Resume",
    acceptedFileTypes: new[] { ".pdf", ".doc", ".docx" },
    maxFileSize: 5 * 1024 * 1024) // 5MB
    
// Multiple file upload
.AddMultipleFileUploadField(x => x.Documents, "Upload Documents",
    maxFiles: 3,
    acceptedFileTypes: new[] { ".pdf", ".jpg", ".png" },
    maxFileSize: 10 * 1024 * 1024) // 10MB per file

The password confirmation validator compares against the rest of the model:

public class PasswordsMatchValidator : IFieldValidator<RegistrationModel, string>
{
    public string? ErrorMessage { get; set; } = "Passwords do not match";

    public Task<ValidationResult> ValidateAsync(
        RegistrationModel model, string value, IServiceProvider services)
        => Task.FromResult(value == model.Password
            ? ValidationResult.Success()
            : ValidationResult.Failure("Passwords do not match"));
}

πŸ› οΈ Advanced Features

Conditional Fields

Show/hide or disable fields based on conditions:

.AddField(x => x.CompanyName, field => field
    .WithLabel("Company Name")
    .VisibleWhen(model => model.UserType == UserType.Business))

.AddField(x => x.TaxId, field => field
    .WithLabel("Tax ID")
    .VisibleWhen(model => model.Country == "US")
    .DisabledWhen(model => model.IsLocked))

For conditional requiredness, use a model-aware validator that only fails when the condition applies:

.AddField(x => x.TaxId, field => field
    .WithLabel("Tax ID")
    .WithValidator(new RequiredWhenUsValidator()))

public class RequiredWhenUsValidator : IFieldValidator<BusinessModel, string>
{
    public string? ErrorMessage { get; set; } = "Tax ID is required for US companies";

    public Task<ValidationResult> ValidateAsync(
        BusinessModel model, string value, IServiceProvider services)
        => Task.FromResult(model.Country == "US" && string.IsNullOrWhiteSpace(value)
            ? ValidationResult.Failure("Tax ID is required for US companies")
            : ValidationResult.Success());
}

Field Groups

Organize related fields into groups with customizable layouts:

var formConfig = FormBuilder<UserModel>
    .Create()
    .AddFieldGroup(group => group
        .WithGroupName("Personal Information")
        .WithColumns(2)  // Two-column layout
        .ShowInCard(2)   // Show in card with elevation 2
        .AddField(x => x.FirstName, field => field
            .WithLabel("First Name")
            .Required())
        .AddField(x => x.LastName, field => field
            .WithLabel("Last Name")
            .Required())
        .AddField(x => x.DateOfBirth))
    .AddFieldGroup(group => group
        .WithGroupName("Contact Information")
        .WithColumns(3)  // Three-column layout
        .ShowInCard()    // Default elevation 1
        .AddField(x => x.Email)
        .AddField(x => x.Phone)
        .AddField(x => x.Address))
    .Build();

Security Features (v2.0.0+)

Configure security settings for your forms:

var formConfig = FormBuilder<SecureForm>.Create()
    .AddField(x => x.SSN, field => field
        .WithLabel("Social Security Number")
        .WithPlaceholder("XXX-XX-XXXX"))
    .AddField(x => x.CreditCard, field => field
        .WithLabel("Credit Card")
        .WithPlaceholder("XXXX XXXX XXXX XXXX"))
    .WithSecurity(security => security
        .EncryptField(x => x.SSN)           // Mark sensitive fields for encryption
        .EncryptField(x => x.CreditCard)
        .EnableCsrfProtection()             // Configure anti-forgery tokens
        .WithRateLimit(5, TimeSpan.FromMinutes(1))  // Max 5 submissions per minute
        .EnableAuditLogging())              // Configure audit logging
    .Build();

How enforcement works (v3.1+): WithSecurity() stores the security settings on the form configuration, AddFormCraft() registers the supporting services (IEncryptionService, ICsrfTokenService, IRateLimitService, IAuditLogService), and FormCraftComponent enforces them automatically: a CSRF token is generated on initialization and validated before OnValidSubmit fires, rate limits are checked (and attempts recorded) before validation, and audit entries (FormSubmitted / FormRejected) are written with excluded and encrypted fields redacted. Blocked submissions show an error alert above the submit button and never reach your handler. Set the SecurityContextId parameter to a per-user value (user id, session id, IP) so rate limits aren't shared across users; it defaults to the model type name. Encryption remains an application concern: call encryptionService.EncryptConfiguredFields(model, config.Security) (or the component's GetEncryptedFieldValues()) to obtain the encrypted values of the marked fields in one call before persisting. Since v3.0.0 the default registration is AES-256 (DefaultEncryptionService) with a random IV per operation β€” configure a 32-byte key for values that must survive a process restart (an ephemeral per-process key is generated otherwise). On WebAssembly a browser-compatible fallback (BlazorEncryptionService, XOR-based obfuscation) is registered instead β€” treat it as obfuscation, not encryption. See the security documentation for details.

Custom Field Renderers

Create specialized input controls for specific field types:

// Create a custom renderer
public class ColorPickerRenderer : CustomFieldRendererBase<string>
{
    public override RenderFragment Render(IFieldRenderContext context)
    {
        return builder =>
        {
            var value = GetValue(context) ?? "#000000";
            
            builder.OpenElement(0, "input");
            builder.AddAttribute(1, "type", "color");
            builder.AddAttribute(2, "value", value);
            builder.AddAttribute(3, "onchange", EventCallback.Factory.CreateBinder<string>(
                this, async (newValue) => await SetValue(context, newValue), value));
            builder.CloseElement();
        };
    }
}

// Use in your form configuration (type arguments: model, value, renderer)
.AddField(x => x.Color, field => field
    .WithLabel("Product Color")
    .WithCustomRenderer<ProductModel, string, ColorPickerRenderer>()
    .WithHelpText("Select the primary color"))

// Register custom renderers (optional for DI)
services.AddScoped<ColorPickerRenderer>();
services.AddScoped<RatingRenderer>();

Built-in example renderers:

  • ColorPickerRenderer - Visual color selection with hex input
  • RatingRenderer - Star-based rating control using MudBlazor

πŸ“Š Performance

FormCraft is designed for optimal performance:

  • ⚑ Minimal re-renders using field-level change detection
  • 🎯 Targeted validation execution
  • πŸ”„ Efficient dependency tracking
  • πŸ“¦ Small bundle size (~50KB gzipped)

πŸ§ͺ Testing

FormCraft is extensively tested with over 880 unit tests covering:

  • βœ… All field types and renderers
  • βœ… Validation scenarios
  • βœ… Field dependencies
  • βœ… Edge cases and error handling
  • βœ… Integration scenarios

🀝 Contributing

We love contributions! Please see our Contributing Guide for details.

Quick Start for Contributors

# Clone the repository
git clone https://github.com/phmatray/FormCraft.git

# Build the project
dotnet build

# Run tests
dotnet test

# Create a local NuGet package
./pack-local.sh  # or pack-local.ps1 on Windows

πŸ“– Documentation

πŸ“š Complete Documentation - Interactive docs with live examples

πŸ—ΊοΈ Roadmap

βœ… Completed

  • File upload field type
  • Security features (encryption, CSRF, rate limiting, audit logging)
  • Modular UI framework architecture
  • Wizard/stepper forms
  • Form templates library (FormTemplates)
  • DateOnly/TimeOnly field support
  • List-of-Values (LOV) modal selection fields
  • Automatic CSRF/rate-limit enforcement in FormCraftComponent (v3.1)
  • Zero-config form generation β€” AddFieldsAuto() (v3.1)
  • Async field dependencies and nullable value-type round-trip (v3.1)

🚧 In Progress

  • Import/Export forms as JSON
  • Rich text editor field

πŸ“‹ Planned

  • Drag-and-drop form builder UI
  • Localization support
  • More layout options
  • Integration with popular CSS frameworks
  • Form state persistence

πŸ’¬ Community

πŸ“„ License

FormCraft is licensed under the MIT License.

πŸ™ Acknowledgments

  • MudBlazor for the amazing component library
  • FluentValidation for validation inspiration
  • The Blazor community for feedback and support

If you find FormCraft useful, please consider giving it a ⭐ on GitHub!

Made with ❀️ by phmatray

About

Build type-safe, dynamic forms in Blazor with an elegant fluent API. FormCraft simplifies complex form creation with automatic field rendering, validation, and dependency management. Features include customizable field renderers, async validation, conditional visibility, and seamless MudBlazor integration.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors