Blazor Tailwind Components

ServiceStack.Blazor high-productivity components enable rapid development in Blazor Server and WASM Apps:

As our components support both hosting models we're maintaining identical Gallery sites running on both Blazor Server and WASM:

For a closer look at ServiceStack.Blazor Components in action, download & run them to see how good they'll run in your Environment:

Dark Mode

All ServiceStack.Blazor components take advantage of Tailwind DarkMode support to include full support for Dark Mode.

Tailwind has revolutionized how we style our Web Apps with its mobile first design system that's dramatically simplified creating maintainable responsive Web Apps. It also excels at adding support for Dark Mode with its first-class dark: modifier allowing the use of standard tailwind classes to specify what elements should look like when viewed in Dark Mode, e.g:

Light mode

Writes Upside-Down

The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.

Dark mode

Writes Upside-Down

The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.

<div class="bg-white dark:bg-slate-800 rounded-lg px-6 py-8 ring-1 ring-slate-900/5 shadow-xl">
  <div>
    <span class="inline-flex items-center justify-center p-2 bg-indigo-500 rounded-md shadow-lg">
      <svg class="h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><!-- ... --></svg>
    </span>
  </div>
  <h3 class="text-slate-900 dark:text-white mt-5 text-base font-medium tracking-tight">Writes Upside-Down</h3>
  <p class="text-slate-500 dark:text-slate-400 mt-2 text-sm">
    The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.
  </p>
</div>

View ServiceStack.Blazor in Dark Mode

We're happy to announce that Dark Mode support is included in all ServiceStack.Blazor components and all Blazor Tailwind project templates where you'll be able to toggle on/off Dark Mode with the new <DarkModeToggle> component. Checkout this video to see how beautiful Dark Mode looks like in the latest ServiceStack.Blazor Components and Tailwind project templates:

For a more interactive view, use the right Dark Mode toggle to turn on/off Dark Mode in the embedded Blazor Gallery Contacts Page:

Dark Mode is all implemented with CSS, controlled by toggling the dark class on the <html class="dark"> element, <DarkModeToggle> also saves this user preference in localStorage where it's preserved across browser restarts.

View in Dark Mode

The Blazor Tailwind templates also include the ability to override the users color scheme preference and open a page in dark or light mode with the ?dark and ?light query params:

Force Dark Mode

If your App is best viewed in Dark Mode you can force it to use Dark Mode with JS.init() when initializing ServiceStack.Blazor's JS library in Blazor Server's _Layout.cshtml or Blazor WASM's index.html, e.g:

<script src="_framework/blazor.server.js"></script>
<script src="/js/servicestack-blazor.js"></script>
<script>JS.init({ colorScheme:'dark' })</script>

Blazor Tailwind Components

Blazor Tailwind Components

We encourage you to explore to the Blazor Gallery websites for the full preview, but we'll look at some of the Components here to give you some idea of the functionality available.

DataGrid

DataGrid is a versatile Component we expect to be heavily used for rendering any typed collection:

<DataGrid Model="Track" Items=@Track.Results />

Which by default renders results in a striped Tailwind Table:

Whose appearance can be styled to support many of the Tailwind Table Styles with the TableStyles Flag enum, e.g:

<DataGrid Model="Track" Items=@Track.Results TableStyle="TableStyle.VerticalLines" />

<DataGrid Model="Track" Items=@Track.Results TableStyle="TableStyle.WhiteBackground" />

<DataGrid Model="Track" Items=@Track.Results TableStyle="TableStyle.FullWidth" />

<DataGrid Model="Track" Items=@Track.Results 
          TableStyle="TableStyle.UppercaseHeadings | TableStyle.FullWidth | TableStyle.VerticalLines" />

It's a highly versatile component where you'll be able to control which columns are displayed and how they're formatted using <Column> definitions, e.g. here's how we can customize the table to look like Blazor's FetchData.cshtml tabular results:

<DataGrid Items=@forecasts class="max-w-screen-md" TableStyle="TableStyle.StripedRows | TableStyle.UppercaseHeadings">
    <Column Field="(WeatherForecast x) => x.Date" Format="dd/MM/yyyy" />
    <Column Title="Temp. (C)" Field="(WeatherForecast x) => x.TemperatureC" />
    <Column Title="Temp. (F)" Field="(WeatherForecast x) => x.TemperatureF" />
    <Column Field="(WeatherForecast x) => x.Summary" />
</DataGrid>

@code {
    List<WeatherForecast> forecasts = new();

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<List<WeatherForecast>>("data/weather.json") ?? new();
    }
}

Here's a more advanced example showing how to implement a responsive DataGrid by utilizing custom Header and Table Cell templates to define what columns and Headers are visible at different responsive breakpoints and how to enable different features like Row Selection and Filtering and examples of handling the Row and Header selected events App's can use for executing custom logic:

<DataGrid Model="Booking" Items=@Items AllowSelection="true" AllowFiltering="true"
          HeaderSelected="HandleSelectedHeader" RowSelected="HandleSelectedRow">
    <Column Field="(Booking x) => x.Id" class="text-gray-900" />
    <Column Field="(Booking x) => x.Name" VisibleFrom="Breakpoint.ExtraLarge" />
    <Column Field="(Booking x) => x.RoomType">
        <Header>
            <span class="hidden lg:inline">Room </span>Type
        </Header>
    </Column>
    <Column Field="(Booking x) => x.RoomNumber">
        <Header>
            <span class="hidden lg:inline">Room </span>No
        </Header>
    </Column>
    <Column Field="(Booking x) => x.Cost" Format="C" />
    <Column Field="(Booking x) => x.BookingStartDate" Formatter="FormatDate" VisibleFrom="Breakpoint.Small">
        <Header>
            Start<span class="hidden lg:inline"> Date</span>
        </Header>
    </Column>
    <Column Field="(Booking x) => x.BookingEndDate" Formatter="FormatDate" VisibleFrom="Breakpoint.ExtraLarge">
        <Header>
            End<span class="hidden lg:inline"> Date</span>
        </Header>
        <Template>@{ var booking = context as Booking; }@booking.BookingEndDate?.ToString("D")
        </Template>
    </Column>
    <Column Title="Employee" Field="(Booking x) => x.CreatedBy" VisibleFrom="Breakpoint.Medium" />
</DataGrid>

@code {
    public List<Booking> Items { get; set; } = new() {
        Create.Booking("First Booking!",  RoomType.Queen,  10, 100, "employee@email.com", "BOOK10"),
        Create.Booking("Booking 2",       RoomType.Double, 12, 120, "manager@email.com",  "BOOK25"),
        Create.Booking("Booking the 3rd", RoomType.Suite,  13, 130, "employee@email.com", "BOOK50"),
    };

    string FormatDate(object o) => o is DateTime d ? d.ToShortDateString() : "";

    public async Task HandleSelectedHeader(Column<Booking> item)
    {
        await JS.Log(item.Name);
    }

    public async Task HandleSelectedRow(Booking x)
    {
        await JS.Log(x);
    }
}

TIP

Resize webpage to preview its responsive appearance and different resolution breakpoints

AutoQueryGrid

The functionality and extensibility in DataGrid lays the foundation for higher-level components like AutoQueryGrid which makes use of it to enable its Auto UI around AutoQuery CRUD Services.

AutoQueryGrid Read Only

At a minimum AutoQueryGrid requires the AutoQuery APIs it should call to implement its functionality, so you can implement a read-only grid by only specifying the AutoQuery API to query a data model, e.g:

<AutoQueryGrid Model="Booking" Apis="Apis.AutoQuery<QueryBookings>()" />

This one AutoQuery API is enough to power a functional read-only UI enabling multi flexible querying capabilities, paging, custom column selection and the ability to export the desired filtered resultset to .csv which can be open in Excel or copy the API URL Apps can use to consume the JSON API results:

AutoQueryGrid CRUD

Full CRUD functionality can be enabled by specifying the AutoQuery CRUD APIs for a specified data model, e.g:

<AutoQueryGrid Model="Booking" Apis="Apis.AutoQuery<QueryBookings,CreateBooking,UpdateBooking,DeleteBooking>()" />

Customizable Columns

As AutoQueryGrid builds on DataGrid it also inherits its customizable option allowing for customizable responsive columns, e.g:

<AutoQueryGrid Model="Booking" Apis="Apis.AutoQuery<QueryBookings,CreateBooking,UpdateBooking,DeleteBooking>()"
               AllowSelection="true" AllowFiltering="true"
               HeaderSelected="OnSelectedHeader" RowSelected="OnSelectedRow">
    <Columns>
        <!-- Custom class -->
        <Column Field="(Booking x) => x.Id" class="text-gray-900" />
        <!-- Only show from Tailwind's xl responsive Breakpoint -->
        <Column Field="(Booking x) => x.Name" VisibleFrom="Breakpoint.ExtraLarge" />
        <!-- Custom Header collapsing 'Room' below 'lg' responsive breakpoint -->
        <Column Field="(Booking x) => x.RoomType">
            <Header>
                <span class="hidden lg:inline">Room </span>Type
            </Header>
        </Column>
        <!-- Custom Header collapsing 'Room' below 'lg' responsive breakpoint -->
        <Column Field="(Booking x) => x.RoomNumber">
            <Header>
                <span class="hidden lg:inline">Room </span>No
            </Header>
        </Column>
        <!-- Custom string Format -->
        <Column Field="(Booking x) => x.Cost" Format="C" />
        <!-- Custom C# Formatter -->
        <Column Field="(Booking x) => x.BookingStartDate" Formatter="FormatDate" VisibleFrom="Breakpoint.Small">
            <Header>
                Start<span class="hidden lg:inline"> Date</span>
            </Header>
        </Column>
        <!-- Custom Header and Cell Value -->
        <Column Field="(Booking x) => x.BookingEndDate" VisibleFrom="Breakpoint.ExtraLarge2x">
            <Header>
                End<span class="hidden lg:inline"> Date</span>
            </Header>
            <Template>
                @context.BookingEndDate?.ToString("D")
            </Template>
        </Column>
        <!-- Custom Title and Complex Type Cell with Reference Link -->
        <Column Title="Voucher" Field="(Booking x) => x.Discount" VisibleFrom="Breakpoint.ExtraLarge">
            <Template>
@if (context.Discount != null)
{
    <TextLink class="flex items-end" href=@($"/gallery/autoquerygrid/coupons?Id={context.Discount.Id}")>
        <PreviewFormat Value=@context.Discount />
    </TextLink>
}          </Template>
        </Column>
    </Columns>
</AutoQueryGrid>

Customizing how and when columns are rendered at different breakpoints using different formatting options and custom table header and cell templates:

Declarative Customizations

The columns can also be customized declaratively using the [Format] Metadata Attribute on the Model type:

public class Contact : AuditBase
{
    [AutoIncrement]
    public int Id { get; set; }

    [Format(FormatMethods.IconRounded)]
    public string ProfileUrl { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    [Format(FormatMethods.Currency)]
    public int? SalaryExpectation { get; set; }

    [Format(FormatMethods.LinkEmail, Options = 
        @"{target:'_self',subject:'New Job Opportunity',
           body:'We have an exciting new opportunity...', cls:'text-green-600'}")]
    public string Email { get; set; }

    [Format(FormatMethods.LinkPhone)]
    public string Phone { get; set; }
}

Which can change how results are formatted in the data grid results:

Whilst the [Input] and [FieldCss] attributes on the AutoQuery CRUD DTOs:

public class UpdateContact : IPatchDb<Contact>, IReturn<Contact>
{
    public int Id { get; set; }

    [ValidateNotEmpty]
    public string? FirstName { get; set; }

    [ValidateNotEmpty]
    public string? LastName { get; set; }

    [Input(Type = "file"), UploadTo("profiles")]
    public string? ProfileUrl { get; set; }
    
    public int? SalaryExpectation { get; set; }

    [ValidateNotEmpty]
    public string? JobType { get; set; }

    public int? AvailabilityWeeks { get; set; }
    public EmploymentType? PreferredWorkType { get; set; }
    public string? PreferredLocation { get; set; }

    [ValidateNotEmpty]
    public string? Email { get; set; }
    public string? Phone { get; set; }
 
    [Input(Type="textarea")]
    [FieldCss(Field="col-span-12 text-center", Input="h-48", Label="text-xl text-indigo-700")]
    public string? About { get; set; }
}

Can customize how forms are rendered, e.g:

ToolbarButtons

The <ToolbarButtons> element can be used to customize the AutoQueryGrid to add your own custom Toolbar buttons, e.g:

<AutoQueryGrid>
  <ToolbarButtons>
    <div class="pl-2"><button>1</button></div>
    <div class="pl-2"><button>2</button></div>
  </ToolbarButtons>
</AutoQueryGrid>

Enabling complete control over the Toolbar as all existing toolbar buttons can be removed with AutoQueryGrid parameters.

Custom Edit and Create Forms

The <CreateForm> and <EditForm> elements can be used to replace the default Auto Forms used in creating and editing rows when more advanced or customized functionality is needed.

With this feature we can create a Custom AutoQueryGrid component that uses Custom Edit & Create Forms when selecting and adding new records and also customize the Grid results displayed with the new ConfigureQuery parameter to ensure results are filtered to the selected Tenant records:

<AutoQueryGrid @ref=@grid Model="Item" Apis="Apis.AutoQuery<QueryItems,NewItem,EditItem>()" ConfigureQuery="Configure">
    <CreateForm>
        <div class="relative z-10">
            <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
                <CustomCreateItem OnClose="grid!.OnEditDone" />
            </div>
        </div>
    </CreateForm>
    <EditForm>
        <div class="relative z-10">
            <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
                <CustomEditItem Item="context" OnClose="grid!.OnEditDone" />
            </div>
        </div>
    </EditForm>
</AutoQueryGrid>

@code {
    AutoQueryGrid<Creative>? grid;
    [Parameter, SupplyParameterFromQuery] public int? TenantId { get; set; }

    void Configure(QueryBase query)
    {
        query.AddQueryParam("TenantId", TenantId);
    }
}

Managing Filters & Preferences

By default the AutoQueryGrid displays the user's selected columns and query limit preferences which are persisted in localStorage. They can be overridden with the new Prefs attribute which has different ergonomic methods for configuration within an attribute:

To limit the Query Results Limit:

<AutoQueryGrid @ref=@grid Model="Contact" Apis="Apis.AutoQuery<QueryContacts,CreateContact,UpdateContact>()"
               Prefs="ApiPrefs.Create(take:10)" />

To limit which columns are displayed in the Query Results:

<AutoQueryGrid @ref=@grid Model="Contact" Apis="Apis.AutoQuery<QueryContacts,CreateContact,UpdateContact>()"
               Prefs="ApiPrefs.Columns(nameof(Contact.Id), nameof(Contact.LastName))" />
<AutoQueryGrid @ref=@grid Model="Contact" Apis="Apis.AutoQuery<QueryContacts,CreateContact,UpdateContact>()"
               Prefs="ApiPrefs.Columns<Contact>(x => new { x.Id, x.LastName, x.Email })" />

To configure both Query Limit and Selected Columns:

<AutoQueryGrid @ref=@grid Model="Contact" Apis="Apis.AutoQuery<QueryContacts,CreateContact,UpdateContact>()"
               Prefs="ApiPrefs.Create(take:10, columns:new(){ nameof(Contact.Id), nameof(Contact.LastName) })" />
<AutoQueryGrid @ref=@grid Model="Contact" Apis="Apis.AutoQuery<QueryContacts,CreateContact,UpdateContact>()"
               Prefs="ApiPrefs.Configure(x => { x.Take = 5; x.SelectedColumns=new() { nameof(Contact.LastName) }; })"/>

In addition the new methods below can be used to clear any user-defined query filters and column preferences:

Method Description
grid.ClearFiltersAsync() Remove user-defined Filters
grid.ResetPreferencesAsync() Remove user-defined Filters and Column Preferences

Disable Column Filtering

By default Filtering and Sorting are disabled for complex type columns, they can also be explicitly disabled per column with AllowFiltering, e.g:

<AutoQueryGrid>
    <Column Field="(Contact x) => x.Phone" AllowFiltering="false" />
<AutoQueryGrid>

Changing AutoQueryGrid Defaults

A lot of AutoQueryGrid's UI is customizable allowing you to easily toggle on/off UI features as needed, if you have a consistent style you wish to enforce you can change the defaults of every AutoQueryGrid component with BlazorConfig, e.g. you can remove Copy URL button and change the default Table style to use Uppercase Headings with:

BlazorConfig.Set(new() {
    //...
    AutoQueryGridDefaults = new() {
        TableStyle = TableStyle.StripedRows | TableStyle.UppercaseHeadings,
        ShowCopyApiUrl = false,
    }
});

Which will change the appearance of every AutoQueryGrid Component used in the App unless overridden.

As AutoQueryGrid is a core component for the rapid development of Apps we're maintaining a dedicated section showcasing their different features at blazor-gallery.servicestack.net/grid:

To provide an optimal UX for relational fields AutoQueryGrid utilizes Modal Lookups for searching and selecting referential data that's automatically inferred from your OrmLite data model relationships, e.g:

public class JobApplication : AuditBase
{
    [AutoIncrement]
    public int Id { get; set; }

    [References(typeof(Job))]
    public int JobId { get; set; }
    
    [References(typeof(Contact))]
    public int ContactId { get; set; }
    //...
}

Where it will display an enhanced LookupInput instead of a plain Text Input for the relational JobId and ContactId fields:

Which users can use to quickly search for the related record instead of manually inserting Foreign Key Ids:

File Uploads

Another feature showcased in the above screenshots is support for Managed File Uploads which can be declaratively added with the [Input(Type="file")] to render the FileInput Component and [UploadTo] attribute to specify which File Upload location it should use:

public class UpdateJobApplication : IPatchDb<JobApplication>, IReturn<JobApplication>
{
    public int Id { get; set; }
    public int? JobId { get; set; }
    public int? ContactId { get; set; }
    public DateTime? AppliedDate { get; set; }
    public JobApplicationStatus? ApplicationStatus { get; set; }

    [Input(Type = "file"), UploadTo("applications")]
    public List<JobApplicationAttachment>? Attachments { get; set; }
}

For a quick primer on using Managed File Uploads to Upload files from Blazor checkout:

Auto Forms

The Auto Form components are other high productivity components which can be used to create an automated form based from a Request DTO definition:

<AutoCreateForm Model="Booking" ApiType="typeof(CreateBooking)" />

AutoEditForm

Whilst AutoEditForm can be used to render an automated form based to update and delete an AutoQuery CRUD API:

<AutoEditForm Model="Booking" Edit="Model" ApiType="typeof(UpdateBooking)" DeleteApiType="typeof(DeleteBooking)" />

@code {
    Booking Model = Create.Booking("First Booking!", RoomType.Queen, 10, 100, "employee@email.com");
}

The forms behavior and appearance is further customizable with the API annotation, declarative validation and the custom Field and Input attributes, e.g:

[Description("Update an existing Booking")]
[Notes("Find out how to quickly create a <a class='svg-external' target='_blank' href='https://youtu.be/rSFiikDjGos'>C# Bookings App from Scratch</a>")]
[Route("/booking/{Id}", "PATCH")]
[ValidateHasRole("Employee")]
[AutoApply(Behavior.AuditModify)]
public class UpdateBooking : IPatchDb<Booking>, IReturn<IdResponse>
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public RoomType? RoomType { get; set; }
    [ValidateGreaterThan(0)]
    public int? RoomNumber { get; set; }
    [ValidateGreaterThan(0)]
    public decimal? Cost { get; set; }
    public DateTime? BookingStartDate { get; set; }
    public DateTime? BookingEndDate { get; set; }
    [Input(Type = "textarea")]
    public string? Notes { get; set; }
    public bool? Cancelled { get; set; }
}

Both AutoForm components will render the Forms UI in a Slide Over dialog and includes built-in support for calling the API to update or edit the record with integrated contextual validation, reporting any field validation errors alongside their Input controls.

AutoFormFields

If more advanced customization of a Forms appearance and behavior is required, you can use AutoFormFields to just render the Form's fields (including Validation binding) that can be used to populate a Request DTO that your App can handle sending, e.g:

<form @onsubmit="submit" @onsubmit:preventDefault>
    <div class="shadow sm:overflow-hidden sm:rounded-md max-w-screen-lg">
        <div class="space-y-6 bg-white py-6 px-4 sm:p-6">
            <div>
                <h3 class="text-lg font-medium leading-6 text-gray-900">@(ApiType.GetDescription())</h3>
                <p class="notes mt-1 text-sm text-gray-500">
                    @((MarkupString)ApiType.GetNotes())
                </p>
            </div>

            <AutoFormFields Type="typeof(Booking)" Api="Api" FormLayout="FormLayout" ModelDictionary="ModelDictionary"/>

        </div>
        <div class="bg-gray-50 px-4 py-3 text-right sm:px-12">
            <PrimaryButton type="submit" onclick="submit">Save</PrimaryButton>
        </div>
    </div>
</form>

@code {
    [Inject] public JsonApiClient? Client { get; set; }
    IHasErrorStatus? Api { get; set; }
    Type ApiType = typeof(UpdateBooking);
    List<InputInfo>? FormLayout { get; set; }
    Dictionary<string, object> ModelDictionary { get; set; } = new();
    MetadataType MetadataType => ApiType.ToMetadataType();

    Booking Edit = Create.Booking("First Booking!", RoomType.Queen, 10, 100, "employee@email.com");

    protected override async Task OnParametersSetAsync()
    {
        await base.OnParametersSetAsync();
        Api = null;

        ModelDictionary = Edit.ToModelDictionary();
        FormLayout ??= MetadataType.CreateFormLayout<Booking>();
    }

    async Task submit()
    {
        var request = ModelDictionary.FromModelDictionary<UpdateBooking>();
        Api = await Client!.ApiAsync(request);
    }
}

Autocomplete

The <Autocomplete> component provides a user friendly Input for being able to search and quickly select items, that includes support for rich templated content, custom matching and infinite scrolling that avoids pre-loading the entire bound list of items.

Instead of being populated with a fixed List of strings or Key Value Pairs the Autocomplete component can bind directly to a list of POCOs to render its templated content where you'll be able to specify a custom Match filter to control which filtered items are displayed, that it can fuzzy match on single or multiple POCO properties.

Single Contact

Here's a simple Autocomplete example that binds to a simple Contact from a List<Contact> in allContacts:

<Autocomplete T="Contact" Options="allContacts" @bind-Value="simple" Label="Single Contact"
    Match="(x, value) => x!.DisplayName.Contains(value, StringComparison.OrdinalIgnoreCase)"
    placeholder="Select Contact">
    <Item>
        <span class="block truncate">@context!.DisplayName</span>
    </Item>
</Autocomplete>

Single Contact with Icon

The item content is templated allowing for rich content which can be used to display a Contact's profile picture and name:

<Autocomplete T="Contact" Options="allContacts" @bind-Value="contact" Label="Single Contact with Icon"
    Match="(x, value) => x!.DisplayName.Contains(value, StringComparison.OrdinalIgnoreCase)"
    placeholder="Select Contact">
    <Item>
        <div class="flex items-center">
            <Icon class="h-6 w-6 flex-shrink-0 rounded-full" Src=@context.ProfileUrl />
            <span class="ml-3 truncate">@context!.DisplayName</span>
        </div>
    </Item>
</Autocomplete>

Multiple Contacts with Icon

It also supports multiple selection by using @bind-Values to bind to the List<Contact> in contacts instead, e.g:

<Autocomplete Options="allContacts" @bind-Values="contacts" Label="Multiple Contacts with Icon"
    Match="(x, value) => x!.DisplayName.Contains(value, StringComparison.OrdinalIgnoreCase)"
    placeholder="Select Contacts">
    <Item>
        <div class="flex items-center">
            <Icon class="h-6 w-6 flex-shrink-0 rounded-full" Src=@context.ProfileUrl />
            <span class="ml-3 truncate">@context!.DisplayName</span>
        </div>
    </Item>
</Autocomplete>

and here's a working example of what they look like together in the same form (example source code):

TagInput

The TagInput component is useful for when you want to manage a list of strings like words or tags - an input that's notably lacking in HTML Forms. Best of all <TagInput> functions like any other input where it can be included and customized in declarative forms.

For example this Update AutoQuery Request DTO:

// Customize Edit Forms with [Input] and [FieldCss] attributes 
public class UpdateContact : IPatchDb<Contact>, IReturn<Contact>
{
    public int Id { get; set; }

    [ValidateNotEmpty]
    public string? FirstName { get; set; }

    [ValidateNotEmpty]
    public string? LastName { get; set; }

    [Input(Type = "file"), UploadTo("profiles")]
    public string? ProfileUrl { get; set; }
    
    public int? SalaryExpectation { get; set; }

    [ValidateNotEmpty]
    public string? JobType { get; set; }

    public int? AvailabilityWeeks { get; set; }
    public EmploymentType? PreferredWorkType { get; set; }
    public string? PreferredLocation { get; set; }

    [ValidateNotEmpty]
    public string? Email { get; set; }
    public string? Phone { get; set; }
    
    [Input(Type = "tag"), FieldCss(Field = "col-span-12")]
    public List<string>? Skills { get; set; }‎}
 
    [Input(Type="textarea")]
    [FieldCss(Field="col-span-12 text-center", Input="h-48", Label="text-xl text-indigo-700")]
    public string? About { get; set; }
}

is all that's needed to render an instantly working API-enabled Form with validation binding using Auto Form components:

<AutoEditForm class=@Class Model="Contact" ApiType="typeof(UpdateContact)" Edit=@contact />

Which by default renders the form in a SlideOver dialog as seen when editing a row in the Contacts AutoQueryGrid component:

Alternatively it can be rendered in a traditional "card" form layout with the new FormStyle.Card option:

<AutoEditForm class=@Class FormStyle="FormStyle.Card" Model="Contact" ApiType="typeof(UpdateContact)" Edit=@contact />

Where it functions the same as other Input components where it can be bound directly to a List<string> Request DTO property:

<form @onsubmit="submit" @onsubmit:preventDefault class=@Class>
<CascadingValue Value=@apiQuery.Error>
    <div class="shadow sm:rounded-md bg-white dark:bg-black">
        <div class="relative px-4 py-5 sm:p-6">
            <fieldset>
                <ErrorSummary Except=@VisibleFields />

                <div class="grid grid-cols-12 gap-6">
                    <div class="col-span-6">
                        <TextInput @bind-Value="request.FirstName" />
                    </div>

                    <div class="col-span-6">
                        <TextInput @bind-Value="request.LastName" />
                    </div>

                    <div class="col-span-12">
                        <TagInput @bind-Value="request.Skills" />
                    </div>
                </div>
            </fieldset>
        </div>
    </div>
</CascadingValue>
</form>

The NavList component encapsulates Tailwind's beautiful List component which is used extensively in Blazor Gallery's Navigation:

<div class="max-w-screen-sm">
    <NavList Title="Explore Blazor Components">
        <NavListItem Title="DataGrid" href="/gallery/datagrid" IconSvg=@Icons.DataGrid>
            DataGrid Component Examples for rendering tabular data
        </NavListItem>
        <NavListItem Title="AutoQuery Grid" href="/gallery/autoquerygrid" IconSvg=@Icons.AutoQueryGrid>
            Instant customizable UIs for calling AutoQuery CRUD APIs
        </NavListItem>
    </NavList>

    <h2 class="mt-8 text-base font-semibold text-gray-500 dark:text-gray-400 flex">
        <span title="Requires Auth"><Icon class="h-6 w-6 mr-2" Svg=@Icons.Padlock /></span>
        Booking APIs
    </h2>
    <NavList>
        <NavListItem Title="Bookings" href="/grid/bookings" Icon=@typeof(Booking).GetIcon()>
            Create and manage Bookings
        </NavListItem>
        <NavListItem Title="Coupons" href="/grid/coupons" Icon=@typeof(Coupon).GetIcon()>
            Create and manage discount Coupons
        </NavListItem>
    </NavList>
</div>

Where it will render a list of navigation links with descriptions and icons:

Colored Buttons

The ButtonStyle on PrimaryButton component can be used to render buttons into Tailwind's different primary colors:

<div class="grid gap-4 grid-cols-3">
    <PrimaryButton>Default</PrimaryButton>
    <PrimaryButton Style="ButtonStyle.Blue">Blue</PrimaryButton>
    <PrimaryButton Style="ButtonStyle.Purple">Purple</PrimaryButton>
    <PrimaryButton Style="ButtonStyle.Red">Red</PrimaryButton>
    <PrimaryButton Style="ButtonStyle.Green">Green</PrimaryButton>
    <PrimaryButton Style="ButtonStyle.Sky">Sky</PrimaryButton>
    <PrimaryButton Style="ButtonStyle.Cyan">Cyan</PrimaryButton>
</div>

Select Input

The <SelectInput> Values and Entries parameters can be used to populate options from an array of string's or KeyValuePair's, it also includes declarative features enabling more capable declarative forms which are typically restricted by the compile-time constant expression limitation of .NET attributes.

The EvalAllowableValues and EvalAllowableEntries attribute properties overcomes this limitation by letting you define the Select options with a #Script Expression whose great .NET scriptability lets you reference your App's .NET instances from a string expression. This feature can then be used to populate declarative Select options from a .NET Instance, e.g:

public class CreateModifier : ICreateDb<Modifier>, IReturn<Modifier>
{
    [ValidateNotEmpty]
    public string Name { get; set; }

    [ValidateNotEmpty]
    [Input(Type="select", EvalAllowableValues = "AppData.Categories")]
    public string Category { get; set; }
 
    public string? Description { get; set; }
}

That we register as a global variable in our AppHost's ScriptContext which we can populate from a dynamic source like a DB Table, e.g:

using var db = container.Resolve<IDbConnectionFactory>().Open();
ScriptContext.Args[nameof(AppData)] = new AppData
{
    Categories = db.Column<string>(db.From<Category>().Select(x => x.Name))
};

Where it will populate the Select input in all CreateModifier Auto Form components:

PreviewFormat

The <PreviewFormat> component is useful for rendering Table Cell data into different customizable formats, e.g:

<PreviewFormat Value="50" Format=Formats.Currency />

<PreviewFormat Value="1000000" Format=Formats.Bytes />

<PreviewFormat Value=@Url Format=Formats.Icon IconClass="w-40 h-40" />

<PreviewFormat Value=@Url Format=Formats.Icon IconClass="w-40 h-40 rounded-full" />

<PreviewFormat Value=@Url Format=Formats.Attachment />

<PreviewFormat Value=@Path Format=Formats.Attachment />

<PreviewFormat Value=@Url Format=Formats.Link />

<PreviewFormat Value=@Email Format=Formats.LinkEmail />

<PreviewFormat Value=@Phone Format=Formats.LinkPhone />

HtmlFormat

Whilst the versatile <HtmlFormat> component can be used to render any Serializable object into a human-friendly HTML Format, e.g:

Single Model

<div class="max-w-screen-sm">
    <HtmlFormat Value=@Track.Results[0] />
</div>

Item Collections

<div class="max-w-screen-sm">
    <HtmlFormat Value=@Track.Results />
</div>

Nested Complex Types

<HtmlFormat Value=@Create.Players />

For more info about the Blazor Components available checkout the Component Gallery:

Blazor Config

A lot of the default conventions used by the Blazor Components are overridable with BlazorConfig initialized in Program.cs, where Blazor WASM projects configured with something like:

BlazorConfig.Set(new BlazorConfig
{
    IsWasm = true,
    Services = app.Services,
    FallbackAssetsBasePath = apiBaseUrl,
    EnableLogging = true,
    EnableVerboseLogging = builder.HostEnvironment.IsDevelopment(),
});

Asset and Fallback Paths

Where FallbackAssetsBasePath allows you to specify a fallback path for Images which is useful when there's a delay for syncing uploaded assets to the CDN that the Blazor WASM client is deployed to, as it can fallback to referencing the asset from the .NET App Server that handled the file upload.

Alternatively AssetsBasePath can be used for specifying a different primary CDN location that's different from the Blazor WASM App CDN or AssetsPathResolver and FallbackPathResolver can be used when more a advanced custom strategy is required.