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

[HxRadioButtonList][HxCheckboxList] Adds Toogle buttons to Radio and Checkbox lists #879

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
10 changes: 8 additions & 2 deletions Havit.Blazor.Components.Web.Bootstrap/Forms/HxCheckbox.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Localization;
using Havit.Blazor.Components.Web.Bootstrap.Internal;
using Microsoft.Extensions.Localization;

namespace Havit.Blazor.Components.Web.Bootstrap;

Expand All @@ -7,7 +8,7 @@ namespace Havit.Blazor.Components.Web.Bootstrap;
/// (Replaces the former <c>HxInputCheckbox</c> component which was dropped in v 4.0.0.)
/// Full documentation and demos: <see href="https://havit.blazor.eu/components/HxCheckbox">https://havit.blazor.eu/components/HxCheckbox</see>
/// </summary>
public class HxCheckbox : HxInputBase<bool>
public class HxCheckbox : HxInputBase<bool>, IInputWithToggleButton
{
/// <summary>
/// Set of settings to be applied to the component instance.
Expand All @@ -19,6 +20,11 @@ public class HxCheckbox : HxInputBase<bool>
/// </summary>
protected override CheckboxSettings GetSettings() => Settings;

/// <summary>
/// Input as toggle or regular.
/// </summary>
[Parameter] public InputAsToggle? InputAsToggle { get; set; }

/// <summary>
/// Text to display next to the checkbox.
/// </summary>
Expand Down
12 changes: 10 additions & 2 deletions Havit.Blazor.Components.Web.Bootstrap/Forms/HxCheckboxList.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Linq.Expressions;
using Havit.Blazor.Components.Web.Bootstrap.Internal;

namespace Havit.Blazor.Components.Web.Bootstrap;

/// <summary>
/// Renders a multi-selection list of <see cref="HxCheckbox"/> controls.<br />
/// Full documentation and demos: <see href="https://havit.blazor.eu/components/HxCheckboxList">https://havit.blazor.eu/components/HxCheckboxList</see>
/// </summary>
public class HxCheckboxList<TValue, TItem> : HxInputBase<List<TValue>> // cannot use an array: https://github.com/dotnet/aspnetcore/issues/15014
public class HxCheckboxList<TValue, TItem> : HxInputBase<List<TValue>>, IInputWithToggleButton // cannot use an array: https://github.com/dotnet/aspnetcore/issues/15014
{
/// <summary>
/// Items to display.
Expand All @@ -19,6 +20,11 @@ public class HxCheckboxList<TValue, TItem> : HxInputBase<List<TValue>> // cannot
/// </summary>
[Parameter] public Func<TItem, string> ItemTextSelector { get; set; }

/// <summary>
/// Input as toggle or regular.
/// </summary>
[Parameter] public InputAsToggle? InputAsToggle { get; set; }

/// <summary>
/// Selects the value from the item.
/// Not required when TValue is the same as TItem.
Expand Down Expand Up @@ -113,6 +119,8 @@ protected override void BuildRenderInput(RenderTreeBuilder builder)

if (_itemsToRender.Count > 0)
{
var inputAsToggleEffective = (this as IInputWithToggleButton).InputAsToggleEffective;

UglyHack uglyHack = new UglyHack(); // see comment below

foreach (var item in _itemsToRender)
Expand All @@ -126,7 +134,7 @@ protected override void BuildRenderInput(RenderTreeBuilder builder)
builder.AddAttribute(4, nameof(HxCheckbox.ValueChanged), EventCallback.Factory.Create<bool>(this, @checked => HandleValueChanged(@checked, item)));
builder.AddAttribute(5, nameof(HxCheckbox.Enabled), EnabledEffective);

builder.AddAttribute(6, nameof(HxCheckbox.CssClass), CssClassHelper.Combine(ItemCssClass, ItemCssClassSelector?.Invoke(item)));
builder.AddAttribute(6, nameof(HxCheckbox.CssClass), CssClassHelper.Combine(ItemCssClass, inputAsToggleEffective == Bootstrap.InputAsToggle.Toggle ? "btn-group" : null, ItemCssClassSelector?.Invoke(item)));
builder.AddAttribute(7, nameof(HxCheckbox.InputCssClass), CssClassHelper.Combine(ItemInputCssClass, ItemInputCssClassSelector?.Invoke(item)));
builder.AddAttribute(8, nameof(HxCheckbox.TextCssClass), CssClassHelper.Combine(ItemTextCssClass, ItemTextCssClassSelector?.Invoke(item)));

Expand Down
20 changes: 16 additions & 4 deletions Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,22 @@ public abstract class HxInputBase<TValue> : InputBase<TValue>, ICascadeEnabledCo
/// <summary>
/// The CSS class to be rendered with the wrapping div.
/// </summary>
private protected virtual string CoreCssClass => CssClassHelper.Combine("hx-form-group position-relative",
((this is IInputWithLabelType inputWithLabelType) && (inputWithLabelType.LabelTypeEffective == LabelType.Floating))
? "form-floating"
: null);
private protected virtual string CoreCssClass
{
get
{
var cssClass = "";
if ((this is IInputWithToggleButton tbutton) && (tbutton.InputAsToggleEffective == InputAsToggle.Toggle))
{
cssClass = CssClassHelper.Combine(cssClass, "btn-group");
}
if ((this is IInputWithLabelType inputWithLabelType) && (inputWithLabelType.LabelTypeEffective == LabelType.Floating))
{
cssClass = CssClassHelper.Combine(cssClass, "form-floating");
}
return CssClassHelper.Combine("hx-form-group position-relative", cssClass);
}
}

/// <summary>
/// The CSS class to be rendered with the input element.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Havit.Blazor.Components.Web.Infrastructure;
using Havit.Blazor.Components.Web.Bootstrap.Internal;
using Havit.Blazor.Components.Web.Infrastructure;

namespace Havit.Blazor.Components.Web.Bootstrap;

Expand All @@ -7,13 +8,18 @@ namespace Havit.Blazor.Components.Web.Bootstrap;
/// </summary>
/// <typeparam name="TValue">Type of value.</typeparam>
/// <typeparam name="TItem">Type of items.</typeparam>
public abstract class HxRadioButtonListBase<TValue, TItem> : HxInputBase<TValue>
public abstract class HxRadioButtonListBase<TValue, TItem> : HxInputBase<TValue>, IInputWithToggleButton
{
/// <summary>
/// Allows grouping radios on the same horizontal row by rendering them inline. Default is <c>false</c>.
/// </summary>
[Parameter] public bool Inline { get; set; }

/// <summary>
/// Input as toggle or regular.
/// </summary>
[Parameter] public InputAsToggle? InputAsToggle { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API is up for discussion in the related issue #887.


/// <summary>
/// Selects a value from an item.
/// Not required when <c>TValueType</c> is the same as <c>TItemTime</c>.
Expand Down Expand Up @@ -134,6 +140,7 @@ protected void BuildRenderInput_RenderRadioItem(RenderTreeBuilder builder, int i
var item = _itemsToRender[index];
if (item != null)
{
var inputAsToggleEffective = (this as IInputWithToggleButton).InputAsToggleEffective;
bool selected = (index == _selectedItemIndex);
if (selected)
{
Expand All @@ -142,13 +149,14 @@ protected void BuildRenderInput_RenderRadioItem(RenderTreeBuilder builder, int i

string inputId = GroupName + "_" + index.ToString();

builder.OpenElement(100, "div");

// TODO CoreCssClass
builder.AddAttribute(101, "class", CssClassHelper.Combine("form-check", Inline ? "form-check-inline" : null, ItemCssClassImpl, ItemCssClassSelectorImpl?.Invoke(item)));

if (inputAsToggleEffective != Bootstrap.InputAsToggle.Toggle)
{
builder.OpenElement(100, "div");
// TODO CoreCssClass
builder.AddAttribute(101, "class", CssClassHelper.Combine("form-check", Inline ? "form-check-inline" : null, ItemCssClassImpl, ItemCssClassSelectorImpl?.Invoke(item)));
}
Comment on lines +152 to +157
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful, this ignores ItemCssClass and ItemCssClassSelector for toggle buttons.
We need to decide whether we can make these work somehow, or if the combination with toggle buttons will be unsupported (in which case, we should probably throw an exception in OnParametersSet).

builder.OpenElement(200, "input");
builder.AddAttribute(201, "class", CssClassHelper.Combine("form-check-input", ItemInputCssClassImpl, ItemInputCssClassSelectorImpl?.Invoke(item)));
builder.AddAttribute(201, "class", CssClassHelper.Combine(inputAsToggleEffective == Bootstrap.InputAsToggle.Toggle ? "btn-check" : "form -check-input", ItemInputCssClassImpl, ItemInputCssClassSelectorImpl?.Invoke(item)));
builder.AddAttribute(202, "type", "radio");
builder.AddAttribute(203, "name", GroupName);
builder.AddAttribute(204, "id", inputId);
Expand All @@ -165,7 +173,7 @@ protected void BuildRenderInput_RenderRadioItem(RenderTreeBuilder builder, int i
builder.CloseElement(); // input

builder.OpenElement(300, "label");
builder.AddAttribute(301, "class", CssClassHelper.Combine("form-check-label", ItemTextCssClassImpl, ItemTextCssClassSelectorImpl?.Invoke(item)));
builder.AddAttribute(301, "class", CssClassHelper.Combine(inputAsToggleEffective == Bootstrap.InputAsToggle.Toggle ? "btn" : "form-check-label", ItemTextCssClassImpl, ItemTextCssClassSelectorImpl?.Invoke(item)));
builder.AddAttribute(302, "for", inputId);
if (ItemTemplateImpl != null)
{
Expand All @@ -177,7 +185,10 @@ protected void BuildRenderInput_RenderRadioItem(RenderTreeBuilder builder, int i
}
builder.CloseElement(); // label

builder.CloseElement(); // div
if (inputAsToggleEffective != Bootstrap.InputAsToggle.Toggle)
{
builder.CloseElement(); // div
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Havit.Blazor.Components.Web.Bootstrap.Internal;
/// <summary>
/// Represents an input with a toggle button option
/// </summary>
public interface IInputWithToggleButton
{
/// <summary>
/// Gets or sets the input as toggle or regular.
/// </summary>
InputAsToggle? InputAsToggle { get; set; }

/// <summary>
/// Gets the effective input type.
/// </summary>
InputAsToggle InputAsToggleEffective => InputAsToggle ?? Bootstrap.InputAsToggle.Regular;
}
15 changes: 15 additions & 0 deletions Havit.Blazor.Components.Web.Bootstrap/InputAsToggle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Havit.Blazor.Components.Web.Bootstrap;
/// <summary>
/// Input display type.
/// </summary>
public enum InputAsToggle
{
/// <summary>
/// Regular
/// </summary>
Regular = 0,
/// <summary>
/// <see href="https://getbootstrap.com/docs/5.3/forms/checks-radios/#toggle-buttons">Toggle buttons</see>.
/// </summary>
Toggle = 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@inject IDemoDataService DemoDataService

<HxCheckboxList TItem="EmployeeDto"
TValue="int"
InputAsToggle="InputAsToggle.Toggle"
Label="Employees"
Data="@data"
ItemTextSelector="@(employee => employee.Name)"
ItemValueSelector="@(employee => employee.Id)"
@bind-Value="selectedEmployeeIds" />

<p class="mt-2">
Selected employee IDs: @String.Join(", ", selectedEmployeeIds.Select(i => i.ToString()) ?? Enumerable.Empty<string>())
</p>

@code
{
private IEnumerable<EmployeeDto> data;
private List<int> selectedEmployeeIds { get; set; } = new();

protected override async Task OnParametersSetAsync()
{
data = await DemoDataService.GetPreferredEmployeesAsync(count: 5);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@
<DocHeading Title="Inline" />
<p>Group checkboxes on the same horizontal row by adding <code>Inline="true"</code>.</p>
<Demo Type="typeof(HxCheckboxList_Demo_Inline)" />

<DocHeading Title="Toggle buttons view" />
<p>Change the look of checkboxes by adding <code>InputAsToggle="InputAsToggle.Toggle"</code></p>
<Demo Type="typeof(HxCheckboxList_Demo_Toggle)" Tabs="false" />

</ComponentApiDoc>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@inject IDemoDataService DemoDataService

<HxRadioButtonList Label="Employee"
TItem="EmployeeDto"
InputAsToggle="InputAsToggle.Toggle"
TValue="int?"
Data="@data"
@bind-Value="@selectedEmployeeId"
ItemTextSelector="@(employee => employee.Name)"
ItemValueSelector="@(employee => employee.Id)" />

<p class="mt-2">Selected employee ID: @selectedEmployeeId</p>

@code {
private IEnumerable<EmployeeDto> data;
private int? selectedEmployeeId;

protected override async Task OnInitializedAsync()
{
data = await DemoDataService.GetPreferredEmployeesAsync(count: 5);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@attribute [Route("/components/" + nameof(HxRadioButtonList<TValue, TItem>))]
@attribute [Route("/components/" + nameof(HxRadioButtonList<TValue, TItem>))]
@attribute [Route("/components/" + nameof(HxRadioButtonListBase<TValue, TItem>))]

<ComponentApiDoc Type="typeof(HxRadioButtonList<TValue, TItem>)">
Expand All @@ -11,4 +11,7 @@
<p>Group radios on the same horizontal row by adding <code>Inline="true"</code>.</p>
<Demo Type="typeof(HxRadioButtonList_Demo_Inline)" />

<DocHeading Title="Toggle buttons view" />
<p>Change the look of radios by adding <code>InputAsToggle="InputAsToggle.Toggle"</code></p>
<Demo Type="typeof(HxRadioButtonList_Demo_Toggle)" Tabs="false" />
</ComponentApiDoc>
Original file line number Diff line number Diff line change
Expand Up @@ -3078,6 +3078,21 @@
Default is <c>null</c> (unlimited).
</summary>
</member>
<member name="T:Havit.Blazor.Components.Web.Bootstrap.Internal.IInputWithToggleButton">
<summary>
Represents an input with a toggle button option
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.IInputWithToggleButton.InputAsToggle">
<summary>
Gets or sets the input as toggle or regular.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.IInputWithToggleButton.InputAsToggleEffective">
<summary>
Gets the effective input type.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.HxInputFileDropZone.Accept">
<summary>
Takes as its value a comma-separated list of one or more file types, or unique file type specifiers, describing which file types to allow.
Expand Down Expand Up @@ -3429,6 +3444,11 @@
Returns an optional set of component settings.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.HxCheckbox.InputAsToggle">
<summary>
Input as toggle or regular.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.HxCheckbox.Text">
<summary>
Text to display next to the checkbox.
Expand Down Expand Up @@ -3509,6 +3529,11 @@
When not set, <c>ToString()</c> is used.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.HxCheckboxList`2.InputAsToggle">
<summary>
Input as toggle or regular.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.HxCheckboxList`2.ItemValueSelector">
<summary>
Selects the value from the item.
Expand Down Expand Up @@ -4775,6 +4800,11 @@
Allows grouping radios on the same horizontal row by rendering them inline. Default is <c>false</c>.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.HxRadioButtonListBase`2.InputAsToggle">
<summary>
Input as toggle or regular.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.HxRadioButtonListBase`2.ItemValueSelectorImpl">
<summary>
Selects a value from an item.
Expand Down Expand Up @@ -7069,6 +7099,21 @@
<member name="M:Havit.Blazor.Components.Web.Bootstrap.HxBootstrapIcon.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder)">
<inheritdoc />
</member>
<member name="T:Havit.Blazor.Components.Web.Bootstrap.InputAsToggle">
<summary>
Input display type.
</summary>
</member>
<member name="F:Havit.Blazor.Components.Web.Bootstrap.InputAsToggle.Regular">
<summary>
Regular
</summary>
</member>
<member name="F:Havit.Blazor.Components.Web.Bootstrap.InputAsToggle.Toggle">
<summary>
<see href="https://getbootstrap.com/docs/5.3/forms/checks-radios/#toggle-buttons">Toggle buttons</see>.
</summary>
</member>
<member name="T:Havit.Blazor.Components.Web.Bootstrap.HxListLayout">
<summary>
Non-generic API for <see cref="T:Havit.Blazor.Components.Web.Bootstrap.HxListLayout`1" />.
Expand Down