dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.36k stars 9.99k forks source link

Exposing the inner ref of an element via BlazorComponent returns an emtpy(?) ref #5670

Closed chanan closed 5 years ago

chanan commented 5 years ago

I have a component that inherits from BlazorComponent with this code:

protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            base.BuildRenderTree(builder);
            builder.OpenElement(0, TagName);
            foreach (var param in _attributesToRender)
            {
                builder.AddAttribute(1, param.Key, param.Value);
            }
            if (_classname != null) builder.AddAttribute(1, "class", _classname);
            builder.AddElementReferenceCapture(2, capturedRef => { ElementRef = capturedRef; });
            builder.AddContent(3, _childContent);
            builder.CloseElement();
        }

It's used like so:

<form onSubmit={this.alertValue}>
    <Dynamic TagName="input" ElementRef="@inputRef" />
        <button type="button" onclick="@OnClick">
            Submit
        </button>
</form>

@functions {
    ElementRef inputRef;

    async void OnClick()
    {
        await inputRef.AlertValue();
    }
}

Which ultimately prints out to the console what the ref is. Like so:

 public static class ElementRefExtensions
    {
        public static async Task AlertValue(this ElementRef elementRef)
        {
            await JSRuntime.Current.InvokeAsync<bool>("blazorousSample.alertValue", elementRef);
        }
    }

window.blazorousSample = {
    alertValue: function (element) {
        console.log("element: %O", element);
        alert(element.value);
    }
};

One the javascript console I get:

{
  "_blazorElementRef": null
}
enetstudio commented 5 years ago

Mate, ElementRef is an opaque reference object holding a reference to an Html element, but it cannot be used in C#. You use it only when you want to pass a reference to an Html element to JavaScript only. The parameter accepted in a JavaScript object, such as a function, behaves just as if it was an object reference retrieved, say, by getElementById()

chanan commented 5 years ago

Yes. I didn’t paste the JavaScript I used but it was a console log. It should be able to print the element ref. So if I was using a input I would expect elementRef.value to work for example.

enetstudio commented 5 years ago

@chanan, you can read about the ElementRef here: https://blazor.net/docs/javascript-interop.html

chanan commented 5 years ago

Thanks, but I know how to use the elementRef. I am trying to expose the html elementRef from my component (code above) to the user of my component so that they can use it themselves. The problem is as I pointed out in the javascript console (shown above) it gets to javascript "empty" (again see log)

enetstudio commented 5 years ago

@chanan,

Once again, the ElementRef is not intended to be used in your C# code. It can and should only be passed to a JavaScript function. Now, if you want to pass an ElementRef to the user of your component, I don't see anything that could stop you from doing it. Just define a property of type ElementRef with get and set accessors, and pass it to whomever you want. But again, the receiver of that reference (meaning element reference) can do nothing with it, such as accessing the properties of the element. The only thing the receiver of your element reference can do is pass it to a JavaScript function...

chanan commented 5 years ago

Yes, i am aware of that and that is what I am doing. The output I showed on my bug is from a console.log in JavaScript.

Because it seems like it was causing some confusion, I updated the bug ticket with the exact javascript code I am using.

CatoLeanTruetschel commented 5 years ago

This is the the value that is marshalled to javascript, if the ElementRef on the C# side does not point to a valid html element. I came accross the same issue when writing a library where a non-present html reference was a valid case and I wanted to handle this case directly in JS. My solution to this is (in JS, adapted to your case):

window.alertValue= function(element) {
   if(!element || element._blazorElementRef === null) {
      // There was no html element reference passed. Handle this case accordingly.
      console.log("No element ref captured.");
   }
   else {
      console.log("element: %O", element);
   }
}

I think in your case, you have to use two way binding of the ElementRef property of your component. The inputRef variable of your outer component is never updated, when the Dynamic component updates its ElementRef property.

chanan commented 5 years ago

@AndreasTruetschel Yes, I think that is what is happening. I am not sure how to fix it though.

I thought you might have meant changing the outer component to:

<Dynamic TagName="input" ElementRef="@inputRef" />

but that throws an error:

ncaught (in promise) Error: System.ArgumentException: 'bind' does not accept values of type Microsoft.AspNetCore.Blazor.ElementRef. To read and write this value type, wrap it in a property of type string with suitable getters and setters.
  at Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValueHandler[T]
CatoLeanTruetschel commented 5 years ago

I cannot follow how the exception is thrown exactly, but for binding to work, you habe to specify the binding in the component that uses your Dynamic component, like

<Dynamic TagName="input" bind-ElementRef="inputRef" />

You don't need the @ here but have to specifiy the bind-.

And in the Dynamic component you need a Parameter that takes an Action<ElementRef> called ElementRefChanged. Then you can invoke the delegate each time the element changes and Blazor will insert an action for the binding appropriately.

This has to look like (in the Dynamic Component):

[Parameter] private ElementRef ElementRef {get;set;}
[Parameter] private Action<ElementRef> ElementRefChanged {get;set;}

and in your BuildRenderTree method:

builder.AddElementReferenceCapture(2, capturedRef => 
{ 
   ElementRef = capturedRef; 
   ElementRefChanged?.Invoke(ElementRef); // Invoke the callback for the binding to work.
});

This is really nothing special and works for any type and not only strings. It is described here (See the Component parameters subsection)

SteveSandersonMS commented 5 years ago

@AndreasTruetschel is correct. You will need bind-something for this to work, otherwise this line of code:

builder.AddElementReferenceCapture(2, capturedRef => { ElementRef = capturedRef; });

... is only writing to its own ElementRef property, not to the corresponding property on the caller.

Since you're writing your component in raw C# instead of Razor, you'll have to figure out what C# code is equivalent to the bind-something that Razor would generate.

SimantoR commented 5 years ago

I believe this should be added to the official Input[*] components that are meant to be used with EditForm. I recently wanted to build a word processor for a complicated form and took me a whole day to find and form a working solution