diff --git a/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs b/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs index ef69c58..ec7385f 100644 --- a/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs +++ b/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs @@ -124,26 +124,42 @@ private RenderFragment RenderField(IFieldConfiguration field) private void RenderSelectField(RenderTreeBuilder builder, IFieldConfiguration field, object? value, object optionsObj) { - builder.OpenComponent>(0); + var property = typeof(TModel).GetProperty(field.FieldName); + var valueType = property?.PropertyType ?? typeof(string); + var underlyingType = Nullable.GetUnderlyingType(valueType) ?? valueType; + + // Use reflection to call the generic helper method with the correct TValue type + var method = typeof(FormCraftComponent) + .GetMethod(nameof(RenderSelectFieldGeneric), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! + .MakeGenericMethod(underlyingType); + + method.Invoke(this, new object?[] { builder, field, value, optionsObj }); + } + + private void RenderSelectFieldGeneric(RenderTreeBuilder builder, IFieldConfiguration field, object? value, object optionsObj) + { + var typedValue = value is TValue tv ? tv : default; + + builder.OpenComponent>(0); AddCommonFieldAttributes(builder, field, 1); - builder.AddAttribute(2, "Value", value?.ToString() ?? string.Empty); + builder.AddAttribute(2, "Value", typedValue); builder.AddAttribute(3, "ValueChanged", - EventCallback.Factory.Create(this, + EventCallback.Factory.Create(this, newValue => UpdateFieldValue(field.FieldName, newValue))); - builder.AddAttribute(11, "ChildContent", RenderSelectOptions(optionsObj)); + builder.AddAttribute(11, "ChildContent", RenderSelectOptions(optionsObj)); builder.CloseComponent(); } - private RenderFragment RenderSelectOptions(object optionsObj) + private RenderFragment RenderSelectOptions(object optionsObj) { return builder => { var sequence = 0; - if (optionsObj is IEnumerable> stringOptions) + if (optionsObj is IEnumerable> typedOptions) { - foreach (var option in stringOptions) + foreach (var option in typedOptions) { - builder.OpenComponent>(sequence++); + builder.OpenComponent>(sequence++); builder.AddAttribute(sequence++, "Value", option.Value); builder.AddAttribute(sequence++, "ChildContent", (RenderFragment)(itemBuilder => itemBuilder.AddContent(0, option.Label))); @@ -160,10 +176,11 @@ private RenderFragment RenderSelectOptions(object optionsObj) if (valueProperty != null && labelProperty != null) { - var optionValue = valueProperty.GetValue(option)?.ToString() ?? ""; + var rawValue = valueProperty.GetValue(option); + var optionValue = rawValue is TValue tv ? tv : default; var optionLabel = labelProperty.GetValue(option)?.ToString() ?? ""; - builder.OpenComponent>(sequence++); + builder.OpenComponent>(sequence++); builder.AddAttribute(sequence++, "Value", optionValue); builder.AddAttribute(sequence++, "ChildContent", (RenderFragment)(itemBuilder => itemBuilder.AddContent(0, optionLabel))); @@ -305,11 +322,27 @@ private async Task UpdateFieldValue(string fieldName, object? value) var property = typeof(TModel).GetProperty(fieldName); if (property != null) { - property.SetValue(Model, value); + var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + var convertedValue = value; + + // Convert value to the target type if necessary + if (value != null && value.GetType() != targetType) + { + try + { + convertedValue = Convert.ChangeType(value, targetType); + } + catch + { + // If conversion fails, use the value as-is + } + } + + property.SetValue(Model, convertedValue); if (OnFieldChanged.HasDelegate) { - await OnFieldChanged.InvokeAsync((fieldName, value)); + await OnFieldChanged.InvokeAsync((fieldName, convertedValue)); } // Handle dependencies diff --git a/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs b/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs index d4b2929..0a16a1f 100644 --- a/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs +++ b/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs @@ -70,6 +70,55 @@ public void WithOptions_Should_Set_Options_Attribute() options.ShouldContain(o => o.Value == "pending" && o.Label == "Pending"); } + [Fact] + public void WithOptions_Should_Set_Int_Options_Attribute() + { + // Arrange & Act + var config = FormBuilder.Create() + .AddField(x => x.Rating, field => field + .WithOptions( + (1, "Low"), + (2, "Medium"), + (3, "High"))) + .Build(); + + // Assert + var field = config.Fields.First(f => f.FieldName == "Rating"); + field.AdditionalAttributes.ShouldContainKey("Options"); + + var options = (field.AdditionalAttributes["Options"] as IEnumerable>)?.ToList(); + options.ShouldNotBeNull(); + options.Count.ShouldBe(3); + options.ShouldContain(o => o.Value == 1 && o.Label == "Low"); + options.ShouldContain(o => o.Value == 2 && o.Label == "Medium"); + options.ShouldContain(o => o.Value == 3 && o.Label == "High"); + } + + [Fact] + public void WithOptions_Should_Preserve_Int_Value_Types() + { + // Arrange & Act + var config = FormBuilder.Create() + .AddField(x => x.Age, field => field + .WithOptions( + (18, "Eighteen"), + (25, "Twenty-Five"), + (65, "Sixty-Five"))) + .Build(); + + // Assert + var field = config.Fields.First(f => f.FieldName == "Age"); + field.AdditionalAttributes.ShouldContainKey("Options"); + + var options = (field.AdditionalAttributes["Options"] as IEnumerable>)?.ToList(); + options.ShouldNotBeNull(); + options.Count.ShouldBe(3); + + // Verify the values are actual ints, not strings + options[0].Value.ShouldBeOfType(); + options[0].Value.ShouldBe(18); + } + [Fact] public void AsMultiSelect_Should_Set_MultiSelectOptions_Attribute() {