Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SelectorItem with boolean value #1816

Merged
merged 2 commits into from
Jun 7, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ public override DotvvmControl CreateControl(PropertyDisplayMetadata property, Au
LocalizableString.CreateNullable(displayAttribute?.Name, displayAttribute?.ResourceType) ??
LocalizableString.Constant(name.Humanize());
var title = LocalizableString.CreateNullable(displayAttribute?.Description, displayAttribute?.ResourceType);
return (name, displayName, title);
})
.Select(e => new SelectorItem(e.displayName.ToBinding(context.BindingService), new(Enum.Parse(enumType, e.name)))
.AddAttribute("title", e.title?.ToBinding(context.BindingService)));
var enumJsString = ReflectionUtils.ToEnumString(enumType, name);

return new SelectorItem(displayName.ToBinding(context.BindingService), new(enumJsString))
.AddAttribute("title", title?.ToBinding(context.BindingService));
});

var control = new ComboBox()
.SetCapability(props.Html)
Expand Down
9 changes: 9 additions & 0 deletions src/Framework/Framework/Binding/ValueOrBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ public TResult ProcessValueBinding<TResult>(DotvvmBindableObject control, Func<T
return processValue(this.Evaluate(control)!);
}

/// <summary> If this contains a `resource` binding, it is evaluated and its value placed in <see cref="ValueOrDefault" /> property. `value`, and all other bindings are untouched and remain in the <see cref="BindingOrDefault"/> property. </summary>
public ValueOrBinding<T?> EvaluateResourceBinding(DotvvmBindableObject control)
{
if (binding is null or IValueBinding or not IStaticValueBinding) return this;

var value = this.Evaluate(control);
return new ValueOrBinding<T?>(value);
}

public static explicit operator ValueOrBinding<T>(T val) => new ValueOrBinding<T>(val);

public const string EqualsDisabledReason = "Equals is disabled on ValueOrBinding<T> as it may lead to unexpected behavior. Please use object.ReferenceEquals for reference comparison or evaluate the ValueOrBinding<T> and compare the value. Or use IsNull/NotNull for nullchecks on bindings.";
Expand Down
11 changes: 10 additions & 1 deletion src/Framework/Framework/Controls/SelectorItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ public SelectorItem(ValueOrBinding<string> text, ValueOrBinding<object> value)

protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
{
writer.AddAttribute("value", Value + "");
var value = this.GetValueOrBinding<object>(ValueProperty).EvaluateResourceBinding(this);
if (value.ValueOrDefault is string s)
{
writer.AddAttribute("value", s);
}
else
{
// anything else than string is better to pass as knockout value binding to avoid issues with `false != 'false'`, ...
writer.AddKnockoutDataBind("value", value.GetJsExpression(this));
}
base.AddAttributesToRender(writer, context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const primitiveTypes: PrimitiveTypes = {
} else if (value === "true" || value === "True") {
return { value: true, wasCoerced: true };
} else if (value === "false" || value === "False") {
return { value: true, wasCoerced: true };
return { value: false, wasCoerced: true };
} else if (typeof value === "number") {
return { value: !!value, wasCoerced: true };
}
Expand Down
22 changes: 21 additions & 1 deletion src/Framework/Framework/Resources/Scripts/tests/coercer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,12 +449,32 @@ test("boolean - valid, converted from number", () => {
expect(result.value).toEqual(true);
})

test("boolean - valid, converted from string", () => {
test("boolean - valid, true converted from string", () => {
const result = tryCoerce("true", "Boolean");
expect(result.wasCoerced).toBeTruthy();
expect(result.value).toEqual(true);
})

test("boolean - valid, false converted from string", () => {
const result = tryCoerce("false", "Boolean");
expect(result.wasCoerced).toBeTruthy();
expect(result.value).toEqual(false);
})
test("boolean - valid, False converted from string", () => {
const result = tryCoerce("False", "Boolean");
expect(result.wasCoerced).toBeTruthy();
expect(result.value).toEqual(false);
})

test("boolean - invalid, invalid string", () => {
const result = tryCoerce("", "Boolean");
expect(result.isError).toBeTruthy();
})
test("boolean - invalid, invalid string", () => {
const result = tryCoerce("bazmek", "Boolean");
expect(result.isError).toBeTruthy();
})

test("boolean - invalid, null", () => {
const result = tryCoerce(null, "Boolean");
expect(result.isError).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.ComboBox
{
public class ComboBoxBooleanViewModel
{
public bool? NullableSelectedValue { get; set; }
public bool NonNullableSelectedValue { get; set; }

public bool[] Items { get; } = new bool[] { true, false };
public bool?[] NullableItems { get; } = new bool?[] { true, false, null };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@viewModel DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.ComboBox.ComboBoxBooleanViewModel

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Hello from DotVVM!</title>
<style>
.invalid {
color: red;
}
</style>
<dot:RequiredResource Name="globalize:cs-CZ" />
</head>
<body>
<p>
Demonstrates ComboBox working when bound to <code>bool</code> or <code>bool?</code>. Each table column should display the same value.
</p>
<table>
<tr>
<th></th>
<th>Nullable boolean</th>
<th>Non-nullable boolean</th>
</tr>
<tr>
<th>Current values</th>
<td data-ui='value-n'>
{{value: NullableSelectedValue == null ? "null" : NullableSelectedValue}}
</td>
<td data-ui='value-nn'>
{{value: NonNullableSelectedValue.ToString()}}
</td>
</tr>
<tr>
<th>DataSource</th>
<td>
<dot:ComboBox DataSource="{value: NullableItems}" SelectedValue="{value: NullableSelectedValue}" data-ui="cb1-n" />
</td>
<td>
<dot:ComboBox DataSource="{value: Items}" SelectedValue="{value: NonNullableSelectedValue}" data-ui="cb1-nn" />
</td>
</tr>
<tr>
<th>Hardcoded items</th>
<td>
<dot:ComboBox SelectedValue="{value: NullableSelectedValue}" data-ui="cb2-n">
<dot:SelectorItem Text="TRUE" Value={value: true} />
<dot:SelectorItem Text="FALSE" Value={value: false} />
<dot:SelectorItem Text="NULL" Value={value: null} />
</dot:ComboBox>
</td>
<td>
<dot:ComboBox SelectedValue="{value: NonNullableSelectedValue}" data-ui="cb2-nn">
<dot:SelectorItem Text="TRUE" Value={value: true} />
<dot:SelectorItem Text="FALSE" Value={value: false} />
</dot:ComboBox>
</td>
</tr>
</table>
</body>
</html>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions src/Samples/Tests/Tests/Control/ComboBoxTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using DotVVM.Samples.Tests.Base;
using DotVVM.Testing.Abstractions;
using OpenQA.Selenium.Support.UI;
using Riganti.Selenium.Core;
using Riganti.Selenium.DotVVM;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace DotVVM.Samples.Tests.Control
{
Expand Down Expand Up @@ -296,5 +298,30 @@ public void Control_ComboBox_BindingCTValidation_StringToEnum()
});
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void Control_ComboBox_BooleanProperty(bool nullable)
{
RunInAllBrowsers(browser => {
browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_ComboBox_BooleanProperty);
var suffix = nullable ? "-n" : "-nn";
var values = nullable ? new bool?[] {true, false, null, false, true} : new bool?[] { true, false, false, true };

foreach (var selectedBox in new [] { "cb1", "cb2" })
{
foreach (var v in values)
{
var index = v switch { true => 0, false => 1, null => 2 };
browser.Single(selectedBox + suffix, SelectByDataUi).Select(index);
AssertUI.InnerTextEquals(browser.Single("value" + suffix, SelectByDataUi), v?.ToString() ?? "null");
Assert.Equal(new SelectElement(browser.Single("cb1" + suffix, SelectByDataUi).WebElement).SelectedOption.Text, v?.ToString().ToLowerInvariant() ?? "");
Assert.Equal(new SelectElement(browser.Single("cb2" + suffix, SelectByDataUi).WebElement).SelectedOption.Text, v?.ToString().ToUpperInvariant() ?? "NULL");
}
}
});
}


}
}