You can create your own component and receive a cascading parameter of type EditContext - you can then use that parameter to invoke validation, and to get any validation messages for your field.
Now that you have full control over the input, you can hook to its @oninput method and do your work (raise other events, do more logic, invoke the .Validate() method of the edit context).
You might need to add some more parameters to allow for value expression, maybe type param, maybe an identifier for the field name, things like that, to make it more reusable and generic, but the core is to get the edit context and control your own rendering and events.
EDIT: added example - note that this is just one implementation and it is neither generic, nor is it perfect
main component with the form
@using System.ComponentModel.DataAnnotations
<EditForm Model="@TheModel">
<DataAnnotationsValidator></DataAnnotationsValidator>
@TheModel.MyProperty
<br />
<MyNumberInput @bind-Value="@TheModel.MyProperty"
MySpecialEvent="@MySpecialEventHandler"
FieldId="@( new FieldIdentifier(TheModel, nameof(MyFormModel.MyProperty)) )" />
<ValidationMessage For="@( () => TheModel.MyProperty )"></ValidationMessage>
</EditForm>
@code{
MyFormModel TheModel { get; set; } = new MyFormModel();
async Task MySpecialEventHandler(DateTime time)
{
Console.WriteLine($"special event fired at: {time.Millisecond}");
}
public class MyFormModel
{
[Required]
[Range(3, 5)]
public int? MyProperty { get; set; }
}
}
child component - MyNumberInput
@implements IDisposable
<input type="number" @oninput="@OnInputHandler" value="@Value" />
@code {
[CascadingParameter]
public EditContext TheEditContext { get; set; }
[Parameter]
public FieldIdentifier FieldId { get; set; }
[Parameter]
public int? Value { get; set; }
[Parameter]
public EventCallback<int?> ValueChanged { get; set; }
[Parameter]
public EventCallback<DateTime> MySpecialEvent { get; set; }
async Task OnInputHandler(ChangeEventArgs e)
{
try
{
int val = int.Parse(e.Value.ToString());
Value = val;
}
catch
{
Value = null;
}
//twoway binding
await ValueChanged.InvokeAsync(Value);
//update validation - AFTER the value is saved in the form
TheEditContext.NotifyFieldChanged(FieldId);
//your own event as needed
await MySpecialEvent.InvokeAsync(DateTime.Now);
}
//some general update that may not be needed but might help if other fields update this one and you need to rerender
void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
{
StateHasChanged();
}
protected override void OnInitialized()
{
if (TheEditContext != null)
TheEditContext.OnValidationStateChanged += ValidationStateChanged;
}
public void Dispose()
{
if (TheEditContext != null)
TheEditContext.OnValidationStateChanged -= ValidationStateChanged;
}
}
The sequence of events is something like
- user types a character
- the input component reacts as it is written (that can be usually the oninput or onchange DOM event - the standard components use onchange, by the way)
- the edit context is notified of the field change and validation is invoked - it comes down as a cascading parameter