Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,42 @@ private RenderFragment RenderField(IFieldConfiguration<TModel, object> field)

private void RenderSelectField(RenderTreeBuilder builder, IFieldConfiguration<TModel, object> field, object? value, object optionsObj)
{
builder.OpenComponent<MudSelect<string>>(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<TModel>)
.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<TValue>(RenderTreeBuilder builder, IFieldConfiguration<TModel, object> field, object? value, object optionsObj)
{
var typedValue = value is TValue tv ? tv : default;

builder.OpenComponent<MudSelect<TValue>>(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<string>(this,
EventCallback.Factory.Create<TValue>(this,
newValue => UpdateFieldValue(field.FieldName, newValue)));
builder.AddAttribute(11, "ChildContent", RenderSelectOptions(optionsObj));
builder.AddAttribute(11, "ChildContent", RenderSelectOptions<TValue>(optionsObj));
builder.CloseComponent();
}

private RenderFragment RenderSelectOptions(object optionsObj)
private RenderFragment RenderSelectOptions<TValue>(object optionsObj)
{
return builder =>
{
var sequence = 0;
if (optionsObj is IEnumerable<SelectOption<string>> stringOptions)
if (optionsObj is IEnumerable<SelectOption<TValue>> typedOptions)
{
foreach (var option in stringOptions)
foreach (var option in typedOptions)
{
builder.OpenComponent<MudSelectItem<string>>(sequence++);
builder.OpenComponent<MudSelectItem<TValue>>(sequence++);
builder.AddAttribute(sequence++, "Value", option.Value);
builder.AddAttribute(sequence++, "ChildContent",
(RenderFragment)(itemBuilder => itemBuilder.AddContent(0, option.Label)));
Expand All @@ -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<MudSelectItem<string>>(sequence++);
builder.OpenComponent<MudSelectItem<TValue>>(sequence++);
builder.AddAttribute(sequence++, "Value", optionValue);
builder.AddAttribute(sequence++, "ChildContent",
(RenderFragment)(itemBuilder => itemBuilder.AddContent(0, optionLabel)));
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestModel>.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<SelectOption<int>>)?.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<TestModel>.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<SelectOption<int>>)?.ToList();
options.ShouldNotBeNull();
options.Count.ShouldBe(3);

// Verify the values are actual ints, not strings
options[0].Value.ShouldBeOfType<int>();
options[0].Value.ShouldBe(18);
}

[Fact]
public void AsMultiSelect_Should_Set_MultiSelectOptions_Attribute()
{
Expand Down
Loading