Search code examples
blazorblazor-webassemblyasp.net-core-6.0

Blazor component to filter keystrokes


Blazor has the InputNumber component that constrains input to digits. However that renders an <input type=number .../> which firefox does not respect (it permits any text).

So I tried to create a custom component that filters input:

@inherits InputNumber<int?>

<input type="number" @bind=_value @onkeypress=OnKeypress />
<p>@_value</p>

@code {
  private string _value;

  private void OnKeypress(KeyboardEventArgs e) {
    if (Regex.IsMatch(e.Key, "[0-9]"))
      _value += e.Key;
  }
}

The <p> shows the correct value. But the input itself shows all keypresses.

How do I write a custom component to filter keystrokes? (In this example I'm filtering for digits, but I assume the same approach would apply for filtering any char.)


Solution

  • If you want to use the Blazor EditForm infrastructure you can create a custom InputNumber.

    The following code inherits from InputNumber and makes the following changes. I've tested it with Firefox and Edge, but don't have Chrome install on my laptop.

    1. The input type is changed to text. This ensures consistent behaviour from all browsers - not the case if set to `number.
    2. Input value is mapped to a new internal field which will update from a new setter.
    3. oninput is wired to SetValue to capture each keypress.
    4. SetValue contains the new logic to check for valid numeric input. The code has inline commentary.
    @inherits InputNumber<TValue>
    @typeparam TValue
    
    <input type="text"
           value="@this.displayValue"
           class="@this.CssClass"
           @oninput=SetValue
           @attributes=this.AdditionalAttributes
           @ref=this.Element />
    
    @code {
        // create a unique string based on the null Ascii char
        //private static string emptyString = ((char)0).ToString();
        private static string emptyString = string.Empty;
        private string? displayValue;
    
        public async Task SetValue(ChangeEventArgs e)
        {
            // Get the current typed value
            var value = e.Value?.ToString();
    
            // Check if it's a number of the TValue or null
            var isValidNumber = BindConverter.TryConvertTo<TValue>(value, System.Globalization.CultureInfo.CurrentCulture, out var num)
                || value is null;
    
             // If it's not valid we need to reset the value
            if (!isValidNumber)
            {
                // Set the value to an empty string
                displayValue = emptyString;
                // Call state has changed to render the component
                StateHasChanged();
                // Give thw renderer some processor time to execute the queued render by creating a Task continuation
                await Task.Delay(1);
                // Set the display to the previous value stored in CurrentValue 
                displayValue = FormatValueAsString(CurrentValue);
                // done
                return;
            }
    
            // We have a numbr so set the two fields to the current value
            // This is the display value
            displayValue = value;
            // This triggers the full InputBase/EditContext logic
            CurrentValueAsString = value;
        }
    }
    

    Why we need the Double Render Trick

    We have to double render if the number in invalid to fix an inconsistency that occurs between the actual DOM and the Render's DOM.

    Consider this:

    1. The current value is 12.
    2. The Renderer's DOM segment is value="12".
    3. We change the input to 12q.
    4. The browser DOM is now value="12q", while the Renderer DOM is still value="12"

    If we now set the Renderer DOM to value="12" it hasn't changed. The Diffing engine sees no difference and doesn't update the browser UI.

    To solve this we have to make sure the Renderer's DOM is set to something else before we set it to the original value. We set it to an empty string, give the renderer some processor time with Task.Delay to actually render the component and then finally set it back to it's original setting.