Substantive.Blazor.DynamicForm 9.4.0-build20260625r1

Substantive.Blazor.DynamicForm

A Blazor library that renders nested object graphs as editable forms using FormCraft and MudBlazor.

Features

  • Builds a deterministic FormNode tree from a model instance using reflection and DisplayAttribute metadata.
  • Renders nested sections (root fields and child nodes) as independent FormCraft forms.
  • Four layout modes: None (flattened inline), Tabs, Stepper, and Sidebar.
  • Attribute-driven field configuration via AttributeFormBuilderExtensions.AddFieldsFromAttributesAndDisplayAttribute<TModel>().
  • DefaultFormConfigurationProvider creates and caches FormCraft configurations per runtime type to avoid repeated reflection.
  • Register a custom IFormConfigurationProvider via dependency injection to override the default configuration strategy.

Requirements

  • .NET 10
  • FormCraft and MudBlazor packages available in the consuming project.

Quick start

  1. Add a project reference to this package or include the project in your solution.
  2. Ensure FormCraft and MudBlazor are registered in your application.
  3. Use NestedFormCraftComponent in a Razor page or component:
@using Substantive.Blazor.DynamicForm

<NestedFormCraftComponent TModel="MyModel"
                          Model="@model"
                          Layout="GroupLayout.Tabs"
                          ShowSubmitButton="true"
                          OnValidSubmit="HandleSubmit" />

@code {
    private MyModel model = new();

    private Task HandleSubmit(MyModel? m)
    {
        // called when any nested section submits successfully
        return Task.CompletedTask;
    }
}

Key component parameters

Parameter Type Default Description
Model TModel? Model instance used to build and populate the form tree.
MaxDepth int 3 Maximum nested discovery depth (clamped to 0–3).
Layout GroupLayout None Controls how top-level sections are presented.
TopLevelGroupTitle string? Presentation-only title override for the synthetic root-fields section ("General" by default).
TopLevelGroupDescription string? Presentation-only description override for the root-fields section.
HeadingTypo MudBlazor.Typo h3 Base heading typography; nested headings scale relative to this value.
ShowSubmitButton bool false When true, nested forms render a submit button.
SubmitButtonText string? Override text for the submit button in nested forms.
OnValidSubmit EventCallback<TModel?> Centralized callback invoked when any nested form validates and submits.

Layout modes

Value Description
GroupLayout.None Sections rendered inline in order (root fields first, then children ordered by Order then Title).
GroupLayout.Tabs Top-level sections rendered as horizontal MudTabs.
GroupLayout.Stepper Top-level sections rendered as sequential MudStepper steps.
GroupLayout.Sidebar Top-level sections listed in a left sidebar; selected section content rendered to the right. Automatically switches to a dialog-based mobile navigation on smaller breakpoints.

Field configuration with AddFieldsFromAttributesAndDisplayAttribute

AttributeFormBuilderExtensions.AddFieldsFromAttributesAndDisplayAttribute<TModel>() scans a model type for field attributes and builds the FormCraft field configuration automatically. It is called internally by DefaultFormConfigurationProvider, so no manual wiring is needed unless you build a custom provider.

Supported field attributes

Attribute Property type Rendered as
[TextField] string Text input
[EmailField] string Email input (with optional format validation)
[TextArea] string Multi-line textarea (supports Rows, MaxLength, AutoResize)
[NumberField] int, decimal, double, float, long Numeric input (supports Min, Max, Step)
[DateField] DateTime, DateTime? Date input (supports MinDate, MaxDate, Format)
[CheckboxField] bool Checkbox
[SelectField] any Select / dropdown (supports inline Options, AllowMultiple, OptionsProviderName)

Properties without one of the above field attributes are not rendered by FormCraft, even if they have a [Display] annotation.

Label and placeholder resolution

The extension follows a deterministic fallback order to keep UI text consistent:

  • Label: DisplayAttribute.Name → field attribute's Label → property name.
  • Placeholder / Prompt: DisplayAttribute.Prompt → field attribute's Placeholder.

Setting [Display(Name = "...")] on a property is therefore the cleanest way to control labels without coupling the attribute to a specific field type.

Validation attributes

The following standard System.ComponentModel.DataAnnotations attributes are applied automatically when present on the property:

Attribute Applied to Effect
[Required] all types Marks the field as required; uses ErrorMessage when provided.
[MinLength(n)] string Adds a minimum-length validator.
[MaxLength(n)] string Adds a maximum-length validator.
[Range(min, max)] numeric types Sets min/max attributes on the input.
[RegularExpression(pattern)] all types Adds a pattern attribute and a regex validator.

Example

using System.ComponentModel.DataAnnotations;
using FormCraft;
using Substantive.Blazor.DynamicForm;

[Display(Name = "User Profile", Description = "Manage account details")]
public class UserProfileModel
{
    // Label comes from DisplayAttribute.Name; prompt from DisplayAttribute.Prompt
    [Display(Name = "Full Name", Prompt = "Enter your full name", Order = 1)]
    [TextField]
    [Required]
    [MaxLength(100)]
    public string? FullName { get; set; }

    // Label falls back to EmailFieldAttribute.Label because no DisplayAttribute.Name is set
    [EmailField(Label = "Email Address", Placeholder = "you@example.com", ValidateFormat = true)]
    [Required]
    public string? Email { get; set; }

    [Display(Name = "Age", Order = 3)]
    [NumberField(Min = 0, Max = 150)]
    [Range(0, 150, ErrorMessage = "Age must be between 0 and 150")]
    public int Age { get; set; }

    [Display(Name = "Bio", Order = 4)]
    [TextArea(Rows = 4, MaxLength = 500)]
    public string? Bio { get; set; }

    [Display(Name = "Country", Order = 5)]
    [SelectField(Options = ["Australia", "Canada", "United Kingdom", "United States"])]
    public string? Country { get; set; }

    [Display(Name = "Subscribe to newsletter", Order = 6)]
    [CheckboxField(Text = "Yes, send me updates")]
    public bool Newsletter { get; set; }

    // Not rendered — no field attribute present
    public string? InternalToken { get; set; }

    // Explicitly hidden from the form
    [Display(AutoGenerateField = false)]
    public string? AuditTrail { get; set; }

    // Complex property → becomes a child section (no field attribute needed)
    [Display(Name = "Address", Description = "Postal address details", Order = 7)]
    public AddressModel Address { get; set; } = new();
}

public class AddressModel
{
    [Display(Name = "Street", Order = 1)]
    [TextField]
    public string? Street { get; set; }

    [Display(Name = "City", Order = 2)]
    [TextField]
    public string? City { get; set; }

    [Display(Name = "Postal Code", Order = 3)]
    [TextField]
    [RegularExpression(@"^\d{4,10}$", ErrorMessage = "Enter a valid postal code")]
    public string? PostalCode { get; set; }
}

With Layout="GroupLayout.Tabs" this model produces two tabs — User Profile and Address — each rendered as an independent FormCraft form.

Configuration

Default (attribute-driven)

DefaultFormConfigurationProvider is used automatically when no IFormConfigurationProvider is registered in DI. It calls AttributeFormBuilderExtensions.AddFieldsFromAttributesAndDisplayAttribute<TModel>() to build a FormCraft configuration from your model's attributes and caches the result per type.

Custom provider via DI

Register an implementation of IFormConfigurationProvider in your service collection to control field layout, validation, and rendering for any model type:

builder.Services.AddSingleton<IFormConfigurationProvider, MyFormConfigurationProvider>();

NestedFormCraftComponent resolves the provider via [Inject] and falls back to DefaultFormConfigurationProvider when none is registered.

Model annotation with DisplayAttribute

DisplayAttribute is the primary way to control how the tree is built and how sections and fields appear in the UI.

Property Effect
Name Sets the section title (for complex properties) or field label. Falls back to the property/type name when absent.
Description Shown as a subtitle under the section heading or field hint.
Order Controls the render order of sections and fields. Lower values appear first. Without Order, sort order is non-deterministic across runtimes.
AutoGenerateField = false Hides the property from the form entirely — no section or field is generated.

AutoGenerateField = true is never required. NestedFormTreeBuilder includes a property when [Display] is absent or when AutoGenerateField is not explicitly set — GetAutoGenerateField() returns null in that case, which is treated as included. Only AutoGenerateField = false changes behaviour.

The DisplayAttribute on the root model type itself sets the title of the root section ("General" by default).

Example

using System.ComponentModel.DataAnnotations;

[Display(Name = "User Profile", Description = "Manage the user's account details")]
public class UserProfileModel
{
    [Display(Name = "Full Name", Order = 1)]
    public string? FullName { get; set; }

    [Display(Name = "Email Address", Order = 2)]
    public string? Email { get; set; }

    // Hidden from the form
    [Display(AutoGenerateField = false)]
    public string? InternalId { get; set; }

    // Complex property → becomes a child section titled "Address"
    [Display(Name = "Address", Description = "Postal address details", Order = 3)]
    public AddressModel Address { get; set; } = new();
}

public class AddressModel
{
    [Display(Name = "Street", Order = 1)]
    public string? Street { get; set; }

    [Display(Name = "City", Order = 2)]
    public string? City { get; set; }

    [Display(Name = "Postal Code", Order = 3)]
    public string? PostalCode { get; set; }
}

With the model above and Layout="GroupLayout.Tabs", the component renders two tabs — User Profile (root fields: Full Name, Email) and Address (Address child section) — in that order.

How it works

  1. NestedFormTreeBuilder inspects the model type using reflection. Complex navigation properties become child FormNode entries; primitive or displayable properties at the root are collected into a synthetic RootFieldsNode titled "General".
  2. NestedFormCraftComponent builds the tree on OnParametersSet, resolves a FormCraft configuration for each node type, and delegates rendering to either the flat NestedFormRenderer (for GroupLayout.None) or a layout component.
  3. Each FormNode owns an independent FormCraftComponent instance. On valid submit, the typed callback forwards the current model value to the top-level OnValidSubmit callback.

References

Showing the top 20 packages that depend on Substantive.Blazor.DynamicForm.

Packages Downloads
Substantive.DevTools.Blazor
Substantive Blazor development tools.
9
Substantive.DevTools.Blazor
Substantive Blazor development tools.
10
Substantive.DevTools.Blazor
Substantive Blazor development tools.
11
Substantive.DevTools.Blazor
Substantive Blazor development tools.
12
Substantive.DevTools.Blazor
Substantive Blazor development tools.
21
Substantive.DevTools.Blazor
Substantive Blazor development tools.
24
Substantive.Metapackage.Blazor
A metapackage contains all Blazor essential building blocks. It packed standalone Blazor components and extensions that often use such as JSInterop, BrowserInterop, Navigation, Exception Handling, Localization, etc,.
55

Version Downloads Last updated
9.4.0-build20260625R1 16 06/25/2026
9.4.0-build20260427R7 8 04/27/2026
9.4.0-build20260427R6 8 04/27/2026
9.4.0-build20260427R5 8 04/27/2026
9.4.0-build20260424R5 9 04/24/2026
9.4.0-build20260424R4 9 04/24/2026
9.4.0-build20260424R3 9 04/24/2026
9.4.0-build20260424R2 9 04/24/2026
9.4.0-build20260424R1 9 04/24/2026
9.4.0-build20260422R1 10 04/22/2026
9.4.0-build20260421R9 11 04/21/2026
9.4.0-build20260421R8 9 04/21/2026
9.4.0-build20260421R6 9 04/21/2026
9.4.0-build20260421R5 9 04/21/2026
9.4.0-build20260421R4 9 04/21/2026
9.4.0-build20260421R3 9 04/21/2026
9.4.0-build20260421R16 8 04/21/2026
9.4.0-build20260421R15 9 04/21/2026
9.4.0-build20260421R14 8 04/21/2026
9.4.0-build20260421R12 8 04/21/2026
9.4.0-build20260421R11 9 04/21/2026
9.4.0-build20260421R10 7 04/21/2026
9.4.0-build20260420R3 9 04/20/2026
9.4.0-build20260420R2 9 04/20/2026
9.4.0-build20260414R2 11 04/14/2026
9.4.0-build20260414R1 10 04/14/2026
9.4.0-build20260410R1 10 04/10/2026
9.4.0-build20260408R1 9 04/08/2026
9.4.0-build20260407R1 10 04/07/2026
9.4.0-build20260403R7 10 04/03/2026
9.4.0-build20260403R6 8 04/03/2026
9.4.0-build20260403R5 8 04/03/2026
9.4.0-build20260401R5 11 04/01/2026
9.4.0-build20260401R4 9 04/01/2026
9.4.0-build20260401R3 9 04/01/2026
9.4.0-build20260401R2 9 04/01/2026
9.4.0-build20260401R1 9 04/01/2026
9.4.0-build20260331R7 9 03/31/2026
9.4.0-build20260331R6 9 03/31/2026
9.4.0-build20260331R5 9 03/31/2026
9.4.0-build20260331R4 9 03/31/2026
9.4.0-build20260331R3 8 03/31/2026
9.4.0-build20260331R2 9 03/31/2026
9.4.0-build20260331R1 9 03/31/2026
9.4.0-build20260330R4 10 03/30/2026
9.4.0-build20260330R3 10 03/30/2026
9.4.0-build20260329R1 9 03/29/2026
9.4.0-build20260318R3 12 03/18/2026
9.4.0-build20260317R2 8 03/17/2026
9.4.0-build20260317R1 6 03/17/2026
9.4.0-build20260217R3 11 02/17/2026
9.4.0-build20260207R3 12 02/07/2026
9.4.0-build20260207R2 8 02/07/2026
9.4.0-build20260206R6 7 02/06/2026
9.4.0-build20260206R5 8 02/06/2026
9.4.0-build20260206R4 8 02/06/2026
9.4.0-build20260206R3 8 02/06/2026
9.4.0-build20260206R2 8 02/06/2026
9.4.0-build20260206R1 10 02/06/2026
9.4.0-build20260205R1 9 02/05/2026
9.4.0-build20260202R1 10 02/02/2026
9.4.0-build20251226R1 54 12/26/2025
9.3.3-build20251226R1 8 12/26/2025