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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Components;

namespace MudExtensions
{
/// <summary>
/// The text field rendered inside <see cref="MudChipField{T}"/>. It validates Required against the chips of
/// the chip field instead of the typed text, so it agrees with the chip field's own validation.
/// It is public only because Razor can't render internal components; it isn't meant to be used directly.
/// </summary>
/// <typeparam name="T"></typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class ChipFieldTextField<T> : MudTextFieldExtended<T>
{
/// <summary>
/// Returns true when the parent chip field has at least one chip.
/// </summary>
[Parameter]
public Func<bool>? HasChips { get; set; }

/// <inheritdoc />
protected override bool HasValue(T? value) => HasChips?.Invoke() ?? base.HasValue(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
@inherits MudTextFieldExtended<T>
@typeparam T

<MudTextFieldExtended @ref="_textFieldExtendedReference"
<ChipFieldTextField @ref="_textFieldExtendedReference"
T="T"
HasChips="@_hasChips"
Immediate="true"
OnKeyDown="HandleKeyDown"
OnKeyUp="HandleKeyUp"
Expand Down Expand Up @@ -63,4 +64,4 @@
@AdornmentEnd
</AdornmentEnd>

</MudTextFieldExtended>
</ChipFieldTextField>
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
_valuesState = registerScope.RegisterParameter<List<string>?>(nameof(Values))
.WithParameter(() => Values)
.WithEventCallback(() => ValuesChanged);
_hasChips = () => HasValue(default);
}

private readonly ParameterState<List<string>?> _valuesState;
private readonly Func<bool> _hasChips;

/// <summary>
/// Protected classes.
Expand Down Expand Up @@ -225,9 +227,9 @@

foreach (var part in parts)
{
if (AllowSameValues || !_valuesState.Value.Contains(part))

Check warning on line 230 in src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
{
_valuesState.Value.Add(part);

Check warning on line 232 in src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
}
}

Expand All @@ -250,7 +252,7 @@
{
await _valuesState.SetValueAsync(new List<string>());
}
_valuesState.Value.Add(ConvertSet(_internalValue) ?? "");

Check warning on line 255 in src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
await ValuesChanged.InvokeAsync(_valuesState.Value);
if (RuntimeLocation.IsServerSide)
{
Expand Down Expand Up @@ -285,6 +287,13 @@
await _textFieldExtendedReference.FocusAsync();
}

/// <summary>
/// The chip field has a value when it has at least one chip, so Required checks the chips instead of the typed text.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
protected override bool HasValue(T? value) => _valuesState.Value is { Count: > 0 };

/// <summary>
/// Clear the text field.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@

Welcome to your new app.

<MudExtensions.UnitTests.TestComponents.MultiSelectTestRequiredValue />

<MudExtensions.UnitTests.TestComponents.DebounceTextFieldTest />
<MudExtensions.UnitTests.TestComponents.ChipFieldRequiredTest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@namespace MudExtensions.UnitTests.TestComponents

<MudForm @ref="Form">
<MudChipField T="string" @bind-Values="Values" Label="Chip Field" Required="true" RequiredError="Add at least one chip" Variant="Variant.Outlined" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => Form?.Validate())">Validate</MudButton>
</MudForm>

@code {
public static string __description__ = "A required chip field should be valid when it has chips, even if its text input is empty";

public MudForm? Form { get; private set; }

[Parameter]
public List<string>? Values { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="_content/CodeBeam.MudBlazor.Extensions/MudExtensions.js"></script>
<script src="_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
</body>

</html>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Bunit;
using AwesomeAssertions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using MudBlazor.Extensions;
using MudExtensions.UnitTests.TestComponents;
Expand Down Expand Up @@ -54,5 +55,144 @@ public async Task ChipField_ParentFormReadOnly_ShouldPreventRemoval()
await comp.InvokeAsync(() => chipField.Instance.Closed(chips[0].Instance));
chipField.Instance.GetState(x => x.Values).Should().BeEquivalentTo(new List<string> { "first", "second" });
}

[Test]
public async Task ChipField_Required_WithChips_ShouldBeValid()
{
var comp = Context.Render<ChipFieldRequiredTest>(opt =>
{
opt.Add(a => a.Values, new List<string> { "first" });
});

await comp.InvokeAsync(() => comp.Instance.Form!.Validate());

comp.Instance.Form!.IsValid.Should().BeTrue();
comp.Instance.Form.Errors.Should().BeEmpty();
comp.FindAll(".mud-input-error").Should().BeEmpty();
comp.Markup.Should().NotContain("Add at least one chip");
}

[Test]
public async Task ChipField_Required_WithoutChips_ShouldBeInvalid()
{
var comp = Context.Render<ChipFieldRequiredTest>(opt =>
{
opt.Add(a => a.Values, new List<string>());
});

await comp.InvokeAsync(() => comp.Instance.Form!.Validate());

comp.Instance.Form!.IsValid.Should().BeFalse();
comp.Instance.Form.Errors.Should().ContainSingle().Which.Should().Be("Add at least one chip");
comp.Markup.Should().Contain("Add at least one chip");
}

[Test]
public async Task ChipField_Required_WithTextButNoChips_ShouldBeInvalid()
{
var comp = Context.Render<ChipFieldRequiredTest>(opt =>
{
opt.Add(a => a.Values, new List<string>());
});

comp.Find("input").Input(new ChangeEventArgs { Value = "not a chip yet" });
await comp.InvokeAsync(() => comp.Instance.Form!.Validate());

comp.Instance.Form!.IsValid.Should().BeFalse();
}

[Test]
public async Task ChipField_Required_TypingAfterError_ShouldKeepShowingError()
{
var comp = Context.Render<ChipFieldRequiredTest>(opt =>
{
opt.Add(a => a.Values, new List<string>());
});
await comp.InvokeAsync(() => comp.Instance.Form!.Validate());
comp.Markup.Should().Contain("Add at least one chip");

comp.Find("input").Input(new ChangeEventArgs { Value = "not a chip yet" });
comp.Markup.Should().Contain("Add at least one chip");

await comp.InvokeAsync(() => comp.Instance.Form!.Validate());

comp.Instance.Form!.IsValid.Should().BeFalse();
comp.Markup.Should().Contain("Add at least one chip");
comp.Find("input").GetAttribute("aria-invalid").Should().Be("true");
}

[Test]
public async Task ChipField_Required_AddingChipAfterError_ShouldBecomeValid()
{
var comp = Context.Render<ChipFieldRequiredTest>(opt =>
{
opt.Add(a => a.Values, new List<string>());
});
await comp.InvokeAsync(() => comp.Instance.Form!.Validate());
comp.Instance.Form!.IsValid.Should().BeFalse();

var chipField = comp.FindComponent<MudChipField<string>>();
comp.Find("input").Input(new ChangeEventArgs { Value = "first" });
await comp.InvokeAsync(() => chipField.Instance.HandleBeforeInput(new BeforeInputEventArgs { Data = " ", InputType = "insert" }));
chipField.Instance.GetState(x => x.Values).Should().BeEquivalentTo(new List<string> { "first" });

await comp.InvokeAsync(() => comp.Instance.Form!.Validate());

comp.Instance.Form!.IsValid.Should().BeTrue();
comp.FindAll(".mud-input-error").Should().BeEmpty();
comp.Markup.Should().NotContain("Add at least one chip");
}

[Test]
public async Task ChipField_Required_BlurWithChips_ShouldNotShowError()
{
var comp = Context.Render<ChipFieldRequiredTest>(opt =>
{
opt.Add(a => a.Values, new List<string> { "first" });
});

await comp.Find("input").BlurAsync(new FocusEventArgs());

comp.FindAll(".mud-input-error").Should().BeEmpty();
comp.Instance.Form!.Errors.Should().BeEmpty();
}

[Test]
public void ChipField_Required_ShouldKeepRequiredMarker()
{
var comp = Context.Render<ChipFieldRequiredTest>(opt =>
{
opt.Add(a => a.Values, new List<string> { "first" });
});

comp.FindAll(".mud-input-control.mud-input-required").Should().NotBeEmpty();
}

[Test]
public async Task ChipField_Required_WithNullValues_ShouldBeInvalid()
{
var comp = Context.Render<ChipFieldRequiredTest>();

await comp.InvokeAsync(() => comp.Instance.Form!.Validate());

comp.Instance.Form!.IsValid.Should().BeFalse();
comp.Markup.Should().Contain("Add at least one chip");
}

[Test]
public async Task ChipFieldTextField_WithoutHasChips_ShouldValidateRequiredAgainstText()
{
var comp = Context.Render<ChipFieldTextField<string>>(opt =>
{
opt.Add(a => a.Required, true);
});

await comp.InvokeAsync(() => comp.Instance.ValidateAsync());
comp.Instance.HasErrors.Should().BeTrue();

comp.Find("input").Change("text");
await comp.InvokeAsync(() => comp.Instance.ValidateAsync());
comp.Instance.HasErrors.Should().BeFalse();
}
}
}
Loading