Skip to content
Open
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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Blazor RadioButton is a basic yet customizable RadioButton component for Tailwindcss.

> **New in v1.1.0**: The library now targets .NET 9 (`net9.0` and `net9.0-browser`) so it can participate in Blazor's new Auto render mode without any extra setup. Drop it into server-side, WebAssembly, or Auto-rendered applications and it will light up interactivity wherever the app is running.

Without passing anything to it you will get a default radio color of `text-blue-500` and `focus:ring-blue-500`. If you would like to implement your own colour please add `radioButtonColor` into the theme section of your tailwind.config.js.

``` javascript
Expand Down Expand Up @@ -65,8 +67,8 @@ Simply open up a component and add your content.
<RadioButtonGroup>
<RadioButtonGroupLabel Label="Values" SubLabel="Sublabel for values!"></RadioButtonGroupLabel>
<RadioButtonGroupContainer GroupName="values" @bind-Value="value" TValue="string">
<RadioButton InputId="value1" Label="Value 1" Value="@("Value1")" TValue="string"></RadioButton>
<RadioButton InputId="value2" Label="Value 2" Value="@("Value2")" TValue="string"></RadioButton>
<RadioButton InputId="value1" Label="Value 1" Value="@("Value1")"></RadioButton>
<RadioButton InputId="value2" Label="Value 2" Value="@("Value2")"></RadioButton>
</RadioButtonGroupContainer>
</RadioButtonGroup>

Expand All @@ -79,7 +81,7 @@ If you would like to add custom content for the ```<RadioButton />``` component,

``` C#
<RadioButton>
<input type="radio" id="value3" value="Value3" name="@(context.GroupName)" @onchange="@(() => { context.SetCurrentValue("Value3"); })" />
<input type="radio" id="value3" value="Value3" name="@(context.GroupName)" @onchange="@(() => context.SetCurrentValue("Value3"))" />
<label for="test3">Value 3</label>
</RadioButton>
```
Expand All @@ -92,7 +94,7 @@ or you can add your custom content inside the ```<RadioButtonGroupContainer />``
</RadioButtonGroupContainer>
```

**NOTE: You must specify the TValue for each ```<RadioButton />``` component or else the compile will fail, this is unfortunately a limitation of Blazor.**
Thanks to Blazor's cascading type inference, you only need to specify `TValue` on the `<RadioButtonGroupContainer>`—child `<RadioButton>` components automatically receive the correct type.

### 3. (Optional) Creating a RadioButton component via a list

Expand Down
52 changes: 31 additions & 21 deletions src/TailBlazor.RadioButton/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
using System;
using System;
using System.Linq;
using System.Reflection;

namespace TailBlazor.RadioButton
namespace TailBlazor.RadioButton;

public static class StringExtensions
{
public static class StringExtensions
{
/// <summary>
/// Check if the string is null or empty.
/// </summary>
/// <param name="s">the string</param>
/// <returns>a ture/false depending of if string is empty</returns>
public static bool IsEmpty(this string s) => string.IsNullOrEmpty(s);
}
/// <summary>
/// Check if the string is null or empty.
/// </summary>
/// <param name="s">the string</param>
/// <returns>true if the string is empty or whitespace, otherwise false.</returns>
public static bool IsEmpty(this string? s) => string.IsNullOrWhiteSpace(s);
}

public static class EnumExtensions
public static class EnumExtensions
{
/// <summary>
/// Return the class attribute for an enum.
/// </summary>
/// <param name="enumValue">the enum</param>
/// <returns>a string based on the class attribute</returns>
public static string GetClass(this Enum enumValue)
{
/// <summary>
/// Return the class attribute for an enum.
/// </summary>
/// <param name="enumValue">the enum</param>
/// <returns>a string based on the class attribute</returns>
public static string GetClass(this Enum enumValue) =>
enumValue
ArgumentNullException.ThrowIfNull(enumValue);

var member = enumValue
.GetType()
.GetMember(enumValue.ToString())
.First()
.GetCustomAttribute<ClassAttribute>().Value;
.FirstOrDefault();

if (member is null)
{
return string.Empty;
}

var attribute = member.GetCustomAttribute<ClassAttribute>();
return attribute?.Value ?? string.Empty;
}
}
74 changes: 41 additions & 33 deletions src/TailBlazor.RadioButton/RadioButton.razor
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
@typeparam TValue
@typeparam TValue
@attribute [CascadingTypeParameter(nameof(TValue))]

<div class="flex items-center @(CheckIfHorizontal())">
@if (ChildContent == null)
<div class="flex items-center @(OrientationClass)">
@if (ChildContent is null)
{
<input type="radio" id="@(InputId)" name="@(RadioButtonGroupContainer?.GroupName)" class="@(ReturnInputClass())" value="@(Value?.ToString().ToLower())" checked="@(RadioButtonGroupContainer.Value?.ToString().ToLower() == Value?.ToString().ToLower())" @onchange="@((ev) => OnChangeHandler(ev))" />
<label for="@(InputId)" class="@(ReturnLabelClass())">@(Label)</label>
<input type="radio"
id="@InputId"
name="@GroupName"
class="@InputCssClass"
value="@ValueAsString"
checked="@IsChecked"
@onchange="OnChangeHandler" />
@if (!Label.IsEmpty())
{
<label for="@InputId" class="@LabelCssClass">@Label</label>
}
}
else
{
@ChildContent(RadioButtonGroupContainer)
@ChildContent(ParentContainer)
}
</div>

Expand All @@ -18,71 +28,69 @@
/// The parent container.
/// </summary>
[CascadingParameter]
protected RadioButtonGroupContainer<TValue> RadioButtonGroupContainer { get; set; }
protected RadioButtonGroupContainer<TValue>? RadioButtonGroupContainer { get; set; }

/// <summary>
/// The id of the radio input.
/// </summary>
[Parameter]
public string InputId { get; set; }
public string? InputId { get; set; }

/// <summary>
/// The class for the radio input. This will override any default classes.
/// </summary>
[Parameter]
public string InputClass { get; set; }
public string? InputClass { get; set; }

/// <summary>
/// The class for the label for the radio input. This will override any default classes.
/// </summary>
[Parameter]
public string LabelClass { get; set; }
public string? LabelClass { get; set; }

/// <summary>
/// The label text.
/// </summary>
[Parameter]
public string Label { get; set; }
public string? Label { get; set; }

/// <summary>
/// The value of the input.
/// </summary>
[Parameter]
public TValue Value { get; set; }
public TValue Value { get; set; } = default!;

/// <summary>
/// The custom content, if not using the pre-made content.
/// </summary>
[Parameter]
public RenderFragment<RadioButtonGroupContainer<TValue>> ChildContent { get; set; }
public RenderFragment<RadioButtonGroupContainer<TValue>>? ChildContent { get; set; }
#endregion

#region private function(s)
/// <summary>
/// Returns the default classes, if InputClass is not empty then return the custom classes.
/// </summary>
/// <returns>a string of classes</returns>
string ReturnInputClass() => InputClass.IsEmpty() ? "focus:ring-blue-500 focus:ring-radioButtonColor h-4 w-4 text-blue-600 text-radioButtonColor border-gray-300" : InputClass;
#region private properties
string InputCssClass => InputClass.IsEmpty() ? "focus:ring-blue-500 focus:ring-radioButtonColor h-4 w-4 text-blue-600 text-radioButtonColor border-gray-300" : InputClass!;

/// <summary>
/// Returns the default classes, if LabelClass is not empty then return the custom classes.
/// </summary>
/// <returns></returns>
string ReturnLabelClass() => LabelClass.IsEmpty() ? "ml-3 block text-sm font-medium text-gray-700" : LabelClass;
string LabelCssClass => LabelClass.IsEmpty() ? "ml-3 block text-sm font-medium text-gray-700" : LabelClass!;

/// <summary>
/// Returns specific margining depending on the orientation of the inputs.
/// </summary>
/// <returns></returns>
string CheckIfHorizontal() => RadioButtonGroupContainer?.Orientation == RadioButtonOrientation.Horizontal ? "mr-3" : "mb-1";
RadioButtonGroupContainer<TValue> ParentContainer => RadioButtonGroupContainer ?? throw new InvalidOperationException("RadioButton must be placed inside a RadioButtonGroupContainer.");

string OrientationClass => ParentContainer.Orientation == RadioButtonOrientation.Horizontal ? "mr-3" : "mb-1";

string GroupName => ParentContainer.GroupName ?? string.Empty;

string ValueAsString => Value is null ? string.Empty : Value.ToString() ?? string.Empty;

bool IsChecked => ParentContainer.IsSelected(Value);
#endregion

#region private function(s)
/// <summary>
/// Set the value in the parent so that we can bind the value.
/// </summary>
/// <param name="ev">the event arguments</param>
void OnChangeHandler(ChangeEventArgs ev)
/// <param name="_">the event arguments</param>
void OnChangeHandler(ChangeEventArgs _)
{
RadioButtonGroupContainer?.SetCurrentValue(this.Value);
}
ParentContainer.SetCurrentValue(Value);
}
#endregion
}
10 changes: 5 additions & 5 deletions src/TailBlazor.RadioButton/RadioButtonGroup.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<fieldset id="@(Id)" class="@(Class)">
<fieldset id="@Id" class="@Class">
@ChildContent
</fieldset>

Expand All @@ -7,17 +7,17 @@
/// The id for the group.
/// </summary>
[Parameter]
public string Id { get; set; }
public string? Id { get; set; }

/// <summary>
/// The class for the group.
/// </summary>
[Parameter]
public string Class { get; set; }
public string? Class { get; set; }

/// <summary>
/// The child content of the group.
/// </summary>
[Parameter]
public RenderFragment ChildContent { get; set; }
}
public RenderFragment? ChildContent { get; set; }
}
52 changes: 32 additions & 20 deletions src/TailBlazor.RadioButton/RadioButtonGroupContainer.razor
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
@typeparam TValue
@typeparam TValue
@attribute [CascadingTypeParameter(nameof(TValue))]

<div class="@(ReturnClass()) @(Orientation.GetClass())">
<CascadingValue Value="this">
@if (Items != null &&
Items?.Count() > 0)
@if (Items?.Any() == true)
{
@foreach(RadioItem<TValue> item in Items)
foreach (var item in Items)
{
<RadioButton TValue="TValue" InputId="@(item.Id)" Value="@(item.Value)" Label="@(item.Text)"></RadioButton>
<RadioButton TValue="TValue" InputId="@item.Id" Value="@item.Value" Label="@item.Text"></RadioButton>
}
}
else
{
@ChildContent(this)
@ChildContent?.Invoke(this)
}
</CascadingValue>
</div>
Expand All @@ -21,7 +21,8 @@
#region private variable(s)
// Defaults
private RadioButtonOrientation _orientation = RadioButtonOrientation.Vertical;
private TValue _value;
private TValue _value = default!;
private bool _hasValue;
#endregion

#region parameter(s)
Expand All @@ -34,23 +35,26 @@
get => _value;
set
{
if (_value?.Equals(value) ?? false) return;
if (_hasValue && EqualityComparer<TValue>.Default.Equals(_value, value))
{
return;
}

_value = value;
_hasValue = true;

// set the value
if(_value != null)
ValueChanged.InvokeAsync(value);
if (ValueChanged.HasDelegate)
{
_ = ValueChanged.InvokeAsync(value);
}
}
}
//[Parameter]
//public TValue Value { get; set; }

/// <summary>
/// The class for the container div. This will override any default classes.
/// </summary>
[Parameter]
public string Class { get; set; }
public string? Class { get; set; }

/// <summary>
/// The orientation that we would like to set these radio buttons.
Expand All @@ -62,7 +66,7 @@
get => _orientation;
set
{
if (Orientation != value)
if (_orientation != value)
{
_orientation = value;
}
Expand All @@ -73,13 +77,13 @@
/// The group name for all the radio buttons that get created.
/// </summary>
[Parameter]
public string GroupName { get; set; }
public string? GroupName { get; set; }

/// <summary>
/// A list of RadioItems so that we can automatically create the radio buttons.
/// </summary>
[Parameter]
public List<RadioItem<TValue>> Items { get; set; }
public IEnumerable<RadioItem<TValue>>? Items { get; set; }

/// <summary>
/// A callback function for any customization when a value changes.
Expand All @@ -91,7 +95,7 @@
/// The child content.
/// </summary>
[Parameter]
public RenderFragment<RadioButtonGroupContainer<TValue>> ChildContent { get; set; }
public RenderFragment<RadioButtonGroupContainer<TValue>>? ChildContent { get; set; }
#endregion

#region public function(s)
Expand All @@ -101,15 +105,23 @@
/// <param name="value"></param>
public void SetCurrentValue(TValue value)
{
this.Value = value;
Value = value;
}

/// <summary>
/// Determines if the provided value matches the currently selected value.
/// </summary>
/// <param name="value">The value to compare.</param>
/// <returns>True when the value is selected.</returns>
public bool IsSelected(TValue value) =>
_hasValue && EqualityComparer<TValue>.Default.Equals(_value, value);
#endregion

#region private function(s)
/// <summary>
/// Returns the default classes, if Class is not empty then return the custom classes.
/// </summary>
/// <returns>a string of classes</returns>
string ReturnClass() => Class.IsEmpty() ? "mt-4 space-y" : Class;
string ReturnClass() => Class.IsEmpty() ? "mt-4 space-y" : Class!;
#endregion
}
Loading