refer to components that created by loop

Viewed 113

I want to create a components by following this steps:

  • I have a list of items.
  • I want to loop in this list and create a component like InputNumber.
  • Add EventCallback to the generic created InputNumber that accept ref of this Inputtext because I want to use this ref to set the focus on this InputNumber.
  • I have also onblure method that execute some code for me, and I am using the onfocus to return focus to the input after execute this code by onblure

My question
How can I get this ref and send it as parameter of EventCallback?
The problem here that this components have been generated by loop, so I don't want to create by hand hundred variables to represent ref's.
My concept code like this:

@code{
  private void OnFocus(MyInputNumber<double?> obj)
  {
     if (obj is not null)
     {
       obj!.Element.Value.FocusAsync();
     }
  }
}
@foreach(var MyItem in MyList)
{
  <EditForm Model="MyItem">
    //Some components ..
    <label>
        Test
        <InputNumber @bind-Value="MyItem.MyVal"
         @onfocus="@((InputNumber<double?> obj @*wrong*@) =>  
    OnFocus(obj))"
    @onblur=@(() => OnblureHandler(context))
    </label>
 </EditForm>
}

If you see up the parameter InputNumber<double?> obj, this way is wrong, usually I use @ref=SomeVariable but becasue I created in generic way, I can not do that.

Note:

I don't to adjust my list to be dictionary<MYItemType,InputNumber<double?>>, or create a new class that has InputNumber<double?> as property. I am searching for different way, like go from editcontext to any input has been modified and reset focus on it, I don't know if that possible !

2 Answers

You can add an InputNumber<double?> InputNumberRef { get; set; } property to your model class. Then is the foreach loop bind it to the component reference @ref="MyItem.InputNumberRef" then you can pass it in your callback method @onblur="() => HandleBlur(MyItem.InputNumberRef)".

Here is the demo code that I used. The following code after input onblur event it waits 2 seconds and returns the focus to the input.

@page "/"

@foreach (var item in _items)
{
    <EditForm Model="@item">
        <InputNumber class="form-control" @ref="@item.InputNumberRef" @bind-Value="@item.Value" @onblur="() => HandleBlur(item.InputNumberRef)" />
    </EditForm>
}

@code {

    private List<Item> _items = new List<Item>
    {
        new Item { Value = 10 },
        new Item { Value = 30 },
        new Item { Value = 20 },
    };

    private async Task HandleBlur(InputNumber<int> inputNumberRef)
    {
        if (inputNumberRef.Element.HasValue)
        {
            await Task.Delay(2000);
            await inputNumberRef.Element.Value.FocusAsync();
        }
    }

    public class Item
    {
        public int Value { get; set; }
        public InputNumber<int> InputNumberRef { get; set; }
    }
}

Credits to user @enet for suggesting this solution in a different question on stackoverflow.

If your requirement is that you apply some form of complex validation on the content of the input before the user is allowed to leave it, i.e if the handler attached to onBlur fails validation then you want to return focus to the input, then this is how to do that without resorting to dictionaries, ...

I've defined a custom InputText component to demonstrate the principles. You'll need to apply the same principles to any other InputBase component where you want to apply the functionality. The key is defining a delegate Func (which returns a bool) as a parameter which is called when the user tries to leave the control. As everything is contained within the component (a bit of SOLID as pointed out by @BrianParker), we can call the inbuilt Element property to return focus to the component.

@inherits InputText

<input @ref="Element"
       @attributes="AdditionalAttributes"
       class="@CssClass"
       value="@CurrentValue"
       @oninput="OnInput"
       @onblur="OnBlur" />

@if (validationMessage != string.Empty)
{
    <div class="validation-message">
        @validationMessage
    </div>
}

@code {
    private string validationMessage = string.Empty;

    [Parameter] public Func<string?, Task<bool>>? BlurValidation { get; set; }

    [Parameter] public string ValidationFailMessage { get; set; } = "Failed Validation";

    private void OnInput(ChangeEventArgs e)
        => this.CurrentValueAsString = e.Value?.ToString() ?? null;

    private async Task OnBlur(FocusEventArgs e)
    {
        validationMessage = string.Empty;
        if (Element is not null && BlurValidation is not null && !await this.BlurValidation.Invoke(this.CurrentValue))
        {
            await Element.Value.FocusAsync();
            validationMessage = ValidationFailMessage;
        }
    }
}

And a demo page:

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

@foreach(var item in model)
{
    <EditForm Model=item>
        <MyInputText class="form-text" @bind-Value=item.MyCountry BlurValidation=CheckBlur />
    </EditForm>
}

@code {
    private List<MyData> model = new List<MyData>() { new MyData { MyCountry = "UK" }, new MyData { MyCountry = "Australia" } };

    private async Task<bool> CheckBlur(string value)
        {
            // Emulate some async behaviour to do whatever checking is required
            await Task.Delay(100);
            // simple test here to demonstrate - I know you could use nornal validation to do this!
            return value.Length > 5;
        }

    public class MyData
    {
        public string? MyCountry { get; set; }
    }
}

I'm not sure I'm happy with the UX using this design, but it's your code.

Related