diff --git a/BitBlazor.sln b/BitBlazor.sln
index 240f987..b80f897 100644
--- a/BitBlazor.sln
+++ b/BitBlazor.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.12.35527.113 d17.12
+# Visual Studio Version 18
+VisualStudioVersion = 18.2.11415.280
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
@@ -47,6 +47,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utilities", "utilities", "{
docs\utilities\icon.md = docs\utilities\icon.md
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "form", "form", "{F518AFB8-A63F-43AA-A476-7580B228283C}"
+ ProjectSection(SolutionItems) = preProject
+ docs\form\checkbox.md = docs\form\checkbox.md
+ docs\form\datepicker.md = docs\form\datepicker.md
+ docs\form\form-components.md = docs\form\form-components.md
+ docs\form\number-field.md = docs\form\number-field.md
+ docs\form\password-field.md = docs\form\password-field.md
+ docs\form\radio.md = docs\form\radio.md
+ docs\form\select-field.md = docs\form\select-field.md
+ docs\form\text-area-field.md = docs\form\text-area-field.md
+ docs\form\text-field.md = docs\form\text-field.md
+ docs\form\timepicker.md = docs\form\timepicker.md
+ docs\form\toggle.md = docs\form\toggle.md
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -103,6 +118,7 @@ Global
{83213F81-05BC-4E9D-8484-95061A10EC05} = {82151F3B-8000-4279-8313-B91EE0FCCE63}
{06530751-809C-4345-9E87-E218FC5B1A03} = {19614F26-5CCD-41B4-9B79-81A8A88217CC}
{8E852CC8-E88D-402A-8F0B-B57794EA8F69} = {19614F26-5CCD-41B4-9B79-81A8A88217CC}
+ {F518AFB8-A63F-43AA-A476-7580B228283C} = {19614F26-5CCD-41B4-9B79-81A8A88217CC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {62D0C14D-C5B1-48FB-AD7E-810A9B818C32}
diff --git a/docs/README.md b/docs/README.md
index 028988e..1d2699e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -97,6 +97,14 @@ Numeric input field component with increment/decrement controls.
- Min/max value constraints
- Custom step values and symbol content
+#### [BitSelectField](form/select-field.md)
+Dropdown select input field component.
+- Generic type support for any value type
+- Support for option grouping
+- Disabled options and groups
+- Form validation integration
+- Accessible and responsive
+
#### [BitCheckbox](form/checkbox.md)
Checkbox component for binary choices.
- Boolean value selection
diff --git a/docs/form/form-components.md b/docs/form/form-components.md
index 900a82f..3888378 100644
--- a/docs/form/form-components.md
+++ b/docs/form/form-components.md
@@ -21,6 +21,7 @@ BitBlazor provides a comprehensive set of form components that integrate with:
| [`BitPasswordField`](password-field.md) | Password input with toggle | Show/hide functionality, secure input |
| [`BitTextAreaField`](text-area-field.md) | Multi-line text input | Configurable rows, auto-sizing |
| [`BitNumberField`](number-field.md) | Numeric input with controls | Increment/decrement buttons, type safety, min/max |
+| [`BitSelectField`](select-field.md) | Dropdown select input | Generic type support, option grouping, form validation |
| [`BitDatepicker`](datepicker.md) | Date input with picker | DateTime/DateOnly support, native browser UI, validation |
| [`BitTimepicker`](timepicker.md) | Time input with picker | TimeOnly support, native browser UI, validation |
| [`BitCheckbox`](checkbox.md) | Boolean checkbox input | Inline/grouped layouts, form validation |
@@ -152,6 +153,18 @@ All input components support three sizes:
@bind-Value="registrationModel.Phone"
For="@(() => registrationModel.Phone)" />
+
+
+ Select your country...
+ Italy
+ United States
+ United Kingdom
+ France
+ Germany
+
+
` component is designed to handle selection from a list of options and provides built-in support for form integration and validation. It is a generic component that can work with any data type supported by Blazor's value converters (or with an appropriate custom converter) and supports option grouping, disabled options, and accessibility attributes.
+
+## Parameters
+
+| Name | Type | Required | Default | Description |
+|------|------|----------|---------|-------------|
+| `Value` | `T?` | ✗ | `null` | The current value of the select field |
+| `ValueChanged` | `EventCallback` | ✗ | - | Callback fired when the value changes |
+| `ValueExpression` | `Expression>?` | ✗ | - | Expression for model binding and validation |
+| `Label` | `string` | ✓ | - | The label text for the select field |
+| `Disabled` | `bool` | ✗ | `false` | Whether the select field is disabled |
+| `ChildContent` | `RenderFragment` | ✓ | - | Content defining the selectable options (BitSelectItem or BitSelectItemGroup) |
+| `AdditionalText` | `RenderFragment?` | ✗ | `null` | Additional descriptive text displayed below the select |
+| `AdditionalTextId` | `string?` | ✗ | `null` | ID for the additional text (used for aria-describedby) |
+| `AdditionalAttributes` | `Dictionary` | ✗ | `{}` | Additional HTML attributes |
+
+## Child Components
+
+### BitSelectItem
+
+Represents an individual selectable option within a BitSelectField component.
+
+**Namespace**: `BitBlazor.Form`
+
+#### Parameters
+
+| Name | Type | Required | Default | Description |
+|------|------|----------|---------|-------------|
+| `Value` | `TValue?` | ✗ | `null` | The value of the option |
+| `ChildContent` | `RenderFragment?` | ✗ | `null` | Content to be rendered as the option text |
+| `Disabled` | `bool` | ✗ | `false` | Whether the option is disabled |
+| `AdditionalAttributes` | `Dictionary` | ✗ | `{}` | Additional HTML attributes |
+
+### BitSelectItemGroup
+
+Represents a group of selectable items within a BitSelectField component.
+
+**Namespace**: `BitBlazor.Form`
+
+#### Parameters
+
+| Name | Type | Required | Default | Description |
+|------|------|----------|---------|-------------|
+| `Label` | `string` | ✓ | - | The label text for the option group |
+| `ChildContent` | `RenderFragment` | ✓ | - | Content defining the options in the group |
+| `Disabled` | `bool` | ✗ | `false` | Whether the entire option group is disabled |
+| `AdditionalAttributes` | `Dictionary` | ✗ | `{}` | Additional HTML attributes |
+
+## Usage Examples
+
+### Basic select field
+
+```razor
+
+ Select a country...
+ Italy
+ United States
+ United Kingdom
+ France
+
+
+@code {
+ private FormModel model = new();
+
+ private class FormModel
+ {
+ public string Country { get; set; } = string.Empty;
+ }
+}
+```
+
+### Select field with validation
+
+```razor
+
+
+
+
+ Select a role...
+ Administrator
+ User
+ Guest
+
+
+
+
+
+ Submit
+
+
+
+@code {
+ private UserModel model = new();
+
+ private class UserModel
+ {
+ [Required(ErrorMessage = "Please select a role")]
+ public string Role { get; set; } = string.Empty;
+ }
+
+ private async Task HandleSubmit()
+ {
+ // Handle form submission
+ }
+}
+```
+
+### Select field with grouped options
+
+```razor
+
+ Choose a browser...
+
+
+ Google Chrome
+ Mozilla Firefox
+ Microsoft Edge
+ Apple Safari
+
+
+
+ Internet Explorer 11
+ Opera
+
+
+
+@code {
+ private BrowserModel model = new();
+
+ private class BrowserModel
+ {
+ public string Browser { get; set; } = string.Empty;
+ }
+}
+```
+
+### Select field with disabled options
+
+```razor
+
+ Select a plan...
+ Free - Basic features
+ Pro - Advanced features
+
+ Enterprise - Contact sales
+
+
+
+@code {
+ private SubscriptionModel model = new();
+
+ private class SubscriptionModel
+ {
+ public string Plan { get; set; } = string.Empty;
+ }
+}
+```
+
+### Select field with numeric values
+
+```razor
+
+ Select priority...
+ Low
+ Medium
+ High
+ Critical
+
+
+@code {
+ private TaskModel model = new();
+
+ private class TaskModel
+ {
+ public int Priority { get; set; }
+ }
+}
+```
+
+### Select field with enum values
+
+```razor
+
+ Pending
+ Processing
+ Shipped
+ Delivered
+ Cancelled
+
+
+@code {
+ private OrderModel model = new() { Status = OrderStatus.Pending };
+
+ private class OrderModel
+ {
+ public OrderStatus Status { get; set; }
+ }
+
+ private enum OrderStatus
+ {
+ Pending,
+ Processing,
+ Shipped,
+ Delivered,
+ Cancelled
+ }
+}
+```
+
+### Select field with additional text
+
+```razor
+
+
+ Select your preferred language for the application interface.
+
+ Select a language...
+ English
+ Italiano
+ Español
+ Français
+
+
+@code {
+ private SettingsModel model = new();
+
+ private class SettingsModel
+ {
+ public string Language { get; set; } = "en";
+ }
+}
+```
+
+### Disabled select field
+
+```razor
+
+ Select payment method...
+ Credit Card
+ PayPal
+ Bank Transfer
+
+
+@code {
+ private PaymentModel model = new();
+
+ private class PaymentModel
+ {
+ public string PaymentMethod { get; set; } = string.Empty;
+ }
+}
+```
+
+## Generated HTML Structure
+
+### Basic select field
+
+```html
+
+
+
+
+```
+
+### Select field with grouped options
+
+```html
+
+
+
+
+```
+
+### Select field with validation error
+
+```html
+
+
+
+
+```
+
+### Select field with disabled option
+
+```html
+
+
+
+
+```
+
+## Generated CSS Classes
+
+### Container classes
+
+- `select-wrapper` - Container wrapper for the entire field
+
+## Accessibility
+
+- Automatically associates labels with select elements using the `for` attribute
+- Supports `aria-describedby` when AdditionalText is provided
+- Maintains focus management and keyboard navigation
+- Compatible with screen readers
+- Supports validation message announcements
+- Option groups enhance screen reader navigation and organization
+- Disabled options are properly announced to assistive technologies
+
+## Form Integration
+
+- Full support for `EditForm` and model binding
+- Compatible with `DataAnnotationsValidator`
+- Integrates with `ValidationSummary` and `ValidationMessage`
+- Supports `For` expression for validation binding
+- Automatic validation state styling
+- Generic type support allows binding to any value type (string, int, enum, etc.)
+
+## Notes
+
+- The component automatically generates unique IDs for form fields using the "select" prefix
+- BitSelectField is a generic component - you can use it with any type: `BitSelectField`, `BitSelectField`, `BitSelectField`, etc.
+- Use BitSelectItem components as direct children to define individual options
+- Use BitSelectItemGroup to organize related options under a common label (rendered as optgroup)
+- The first option typically serves as a placeholder (with empty or default value)
+- Options and groups can be individually disabled
+- The component extends `BitFormComponentBase` for consistent form behavior
+- All Bootstrap Italia form styling is automatically applied
+- The component supports all standard HTML select attributes through `AdditionalAttributes`
+- When working with enums, ensure you pass enum values directly to BitSelectItem without converting to string
diff --git a/docs/quick-reference.md b/docs/quick-reference.md
index 50ebe41..9dbe37c 100644
--- a/docs/quick-reference.md
+++ b/docs/quick-reference.md
@@ -154,6 +154,34 @@ This guide provides a quick overview of all BitBlazor components with basic exam
Step="1" />
```
+### BitSelectField
+```razor
+
+
+ Select a country...
+ Italy
+ United States
+ United Kingdom
+
+
+
+
+ Choose a browser...
+
+ Google Chrome
+ Mozilla Firefox
+ Microsoft Edge
+
+
+
+
+
+ Pending
+ Processing
+ Shipped
+
+```
+
### BitCheckbox
```razor
@@ -379,6 +407,18 @@ Size.Large // Large
@bind-Value="model.Phone"
For="@(() => model.Phone)" />
+
+
+ Select a country...
+ Italy
+ United States
+ United Kingdom
+ France
+ Germany
+
+
///
- /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs
- /// that are not explicitly defined in the component's parameters. The keys represent attribute names, and the
- /// values represent their corresponding values.
+ /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs that are not explicitly defined in the component's parameters.
+ /// The keys represent attribute names, and the values represent their corresponding values.
///
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary AdditionalAttributes { get; set; } = new Dictionary();
diff --git a/src/BitBlazor/Form/BitFormComponentBase.cs b/src/BitBlazor/Form/BitFormComponentBase.cs
index f42eb17..309043b 100644
--- a/src/BitBlazor/Form/BitFormComponentBase.cs
+++ b/src/BitBlazor/Form/BitFormComponentBase.cs
@@ -63,12 +63,6 @@ public abstract class BitFormComponentBase : BitComponentBase
[Parameter]
public bool Disabled { get; set; }
- ///
- /// Gets or sets the placeholder to show in the component
- ///
- [Parameter]
- public string? Placeholder { get; set; }
-
///
/// Gets or sets an optional fragment of additional content to render.
///
@@ -102,7 +96,7 @@ public abstract class BitFormComponentBase : BitComponentBase
///
protected BitFormComponentBase()
{
- if (!SupportedTypes.Contains(ComponentType))
+ if (SupportedTypes.Length > 0 && !SupportedTypes.Contains(ComponentType))
{
throw new NotSupportedException("Type not supported");
}
@@ -123,22 +117,9 @@ protected override void OnParametersSet()
{
base.OnParametersSet();
SetRequiredAttribute();
- SetPlaceholderAttribute();
SetAdditionalTextAttributes();
}
- private void SetPlaceholderAttribute()
- {
- if (!string.IsNullOrWhiteSpace(Placeholder))
- {
- AdditionalAttributes["placeholder"] = Placeholder;
- }
- else
- {
- AdditionalAttributes.Remove("placeholder");
- }
- }
-
private void SetRequiredAttribute()
{
if (Required)
diff --git a/src/BitBlazor/Form/BitInputFieldBase.cs b/src/BitBlazor/Form/BitInputFieldBase.cs
index 62ba74d..05a1d37 100644
--- a/src/BitBlazor/Form/BitInputFieldBase.cs
+++ b/src/BitBlazor/Form/BitInputFieldBase.cs
@@ -27,6 +27,12 @@ public abstract class BitInputFieldBase : BitFormComponentBase
[Parameter]
public bool Plaintext { get; set; }
+ ///
+ /// Gets or sets the placeholder to show in the component
+ ///
+ [Parameter]
+ public string? Placeholder { get; set; }
+
///
/// Gets or sets the size of the text field component.
///
@@ -41,6 +47,25 @@ public abstract class BitInputFieldBase : BitFormComponentBase
private bool isLabelActive = false;
+ ///
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+ SetPlaceholderAttribute();
+ }
+
+ private void SetPlaceholderAttribute()
+ {
+ if (!string.IsNullOrWhiteSpace(Placeholder))
+ {
+ AdditionalAttributes["placeholder"] = Placeholder;
+ }
+ else
+ {
+ AdditionalAttributes.Remove("placeholder");
+ }
+ }
+
///
/// Sets the active state of the label.
///
diff --git a/src/BitBlazor/Form/SelectField/BitSelectField.razor b/src/BitBlazor/Form/SelectField/BitSelectField.razor
new file mode 100644
index 0000000..25715c6
--- /dev/null
+++ b/src/BitBlazor/Form/SelectField/BitSelectField.razor
@@ -0,0 +1,23 @@
+@namespace BitBlazor.Form
+
+@typeparam T
+@inherits BitFormComponentBase
+
+
\ No newline at end of file
diff --git a/src/BitBlazor/Form/SelectField/BitSelectField.razor.cs b/src/BitBlazor/Form/SelectField/BitSelectField.razor.cs
new file mode 100644
index 0000000..18dfbec
--- /dev/null
+++ b/src/BitBlazor/Form/SelectField/BitSelectField.razor.cs
@@ -0,0 +1,36 @@
+using BitBlazor.Core;
+using Microsoft.AspNetCore.Components;
+
+namespace BitBlazor.Form;
+
+///
+/// Represents a select field component.
+///
+///
+/// Use to define the selectable options for the field.
+/// This component is intended for use within Bit form layouts and renders options using the native HTML <select> element,
+/// so BitSelectItem option content should be plain text only (nested markup inside options is not supported).
+/// The type of the value represented and selected by the field.
+public partial class BitSelectField : BitFormComponentBase, IBitSelectField
+{
+ ///
+ protected override string FieldIdPrefix { get; } = "select";
+
+ ///
+ protected override Type[] SupportedTypes { get; } = [];
+
+ ///
+ /// Gets or sets the content to be rendered inside the component.
+ ///
+ [Parameter]
+ [EditorRequired]
+ public RenderFragment ChildContent { get; set; } = default!;
+
+ private string ComputeContainerCssClass()
+ {
+ var builder = new CssClassBuilder("select-wrapper");
+ AddCustomCssClass(builder);
+
+ return builder.Build();
+ }
+}
diff --git a/src/BitBlazor/Form/SelectField/BitSelectItem.razor b/src/BitBlazor/Form/SelectField/BitSelectItem.razor
new file mode 100644
index 0000000..c054565
--- /dev/null
+++ b/src/BitBlazor/Form/SelectField/BitSelectItem.razor
@@ -0,0 +1,7 @@
+@namespace BitBlazor.Form
+
+@typeparam TValue
+
+
diff --git a/src/BitBlazor/Form/SelectField/BitSelectItem.razor.cs b/src/BitBlazor/Form/SelectField/BitSelectItem.razor.cs
new file mode 100644
index 0000000..189f753
--- /dev/null
+++ b/src/BitBlazor/Form/SelectField/BitSelectItem.razor.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Components;
+
+namespace BitBlazor.Form;
+
+///
+/// Represents an individual selectable option within a BitSelectField component.
+///
+///
+/// Use BitSelectItem as a child of BitSelectField to define selectable options.
+/// The Value property specifies the underlying value for the option, and ChildContent defines the display content shown to the user.
+///
+/// The type of the value associated with the select item.
+public partial class BitSelectItem
+{
+ [CascadingParameter]
+ BitSelectField Parent { get; set; } = default!;
+
+ ///
+ /// Gets or sets the value of the option.
+ ///
+ [Parameter]
+ public TValue? Value { get; set; }
+
+ ///
+ /// Gets or sets the content to be rendered inside the component.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ ///
+ /// Gets or sets whether the option is disabled
+ ///
+ [Parameter]
+ public bool Disabled { get; set; }
+
+ ///
+ /// Gets or sets additional attributes that do not match any of the explicitly defined parameters.
+ ///
+ ///
+ /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs that are not explicitly defined in the component's parameters.
+ /// The keys represent attribute names, and the values represent their corresponding values.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IDictionary AdditionalAttributes { get; set; } = new Dictionary();
+}
diff --git a/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor
new file mode 100644
index 0000000..2e2c663
--- /dev/null
+++ b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor
@@ -0,0 +1,5 @@
+@namespace BitBlazor.Form
+
+
diff --git a/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor.cs b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor.cs
new file mode 100644
index 0000000..e1c27a6
--- /dev/null
+++ b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor.cs
@@ -0,0 +1,47 @@
+using Microsoft.AspNetCore.Components;
+
+namespace BitBlazor.Form;
+
+///
+/// Represents a group of selectable items within a BitSelectField component.
+///
+///
+/// Use BitSelectItemGroup to organize related BitSelectItem components under a common label within a BitSelectField component.
+/// This enhances accessibility and user experience by grouping options logically.
+/// The group label is typically rendered as an optgroup label in the resulting markup.
+///
+public partial class BitSelectItemGroup
+{
+ [CascadingParameter]
+ IBitSelectField Parent { get; set; } = default!;
+
+ ///
+ /// Gets or sets the text label displayed for the component.
+ ///
+ [Parameter]
+ [EditorRequired]
+ public string Label { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the content to be rendered inside the component.
+ ///
+ [Parameter]
+ [EditorRequired]
+ public RenderFragment ChildContent { get; set; } = default!;
+
+ ///
+ /// Gets or sets whether the option group is disabled
+ ///
+ [Parameter]
+ public bool Disabled { get; set; }
+
+ ///
+ /// Gets or sets additional attributes that do not match any of the explicitly defined parameters.
+ ///
+ ///
+ /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs that are not explicitly defined in the component's parameters.
+ /// The keys represent attribute names, and the values represent their corresponding values.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IDictionary AdditionalAttributes { get; set; } = new Dictionary();
+}
diff --git a/src/BitBlazor/Form/SelectField/IBitSelectField.cs b/src/BitBlazor/Form/SelectField/IBitSelectField.cs
new file mode 100644
index 0000000..a5ca674
--- /dev/null
+++ b/src/BitBlazor/Form/SelectField/IBitSelectField.cs
@@ -0,0 +1,11 @@
+namespace BitBlazor.Form;
+
+///
+/// Represents a generic select field.
+///
+///
+/// This is a marker interface and it's intended for internal use and is not intended to be implemented or used directly in application code.
+///
+internal interface IBitSelectField
+{
+}
\ No newline at end of file
diff --git a/stories/BitBlazor.Stories/BitBlazor.Stories.csproj b/stories/BitBlazor.Stories/BitBlazor.Stories.csproj
index aac5ac2..1ef21b8 100644
--- a/stories/BitBlazor.Stories/BitBlazor.Stories.csproj
+++ b/stories/BitBlazor.Stories/BitBlazor.Stories.csproj
@@ -9,9 +9,9 @@
-
-
-
+
+
+ allruntime; build; native; contentfiles; analyzers
diff --git a/stories/BitBlazor.Stories/Components/Stories/Form/BitSelectField.stories.razor b/stories/BitBlazor.Stories/Components/Stories/Form/BitSelectField.stories.razor
new file mode 100644
index 0000000..f4a0a75
--- /dev/null
+++ b/stories/BitBlazor.Stories/Components/Stories/Form/BitSelectField.stories.razor
@@ -0,0 +1,77 @@
+@attribute [Stories("Form/BitSelectField")]
+
+
+
+
+
+
+
+
+
+
+
+
+
+