2

The problem concerns a client side Blazor compoenent. The component contains a div hidden by a component variable (bool opened).

I need the component to run some Javascript after the div has seen shown in the compoenent code file (in order to adjust it's position on the screen), code below should explain this a little better:

Component.razor

<div id="select-@Id" class="select-component" style="position: relative;">
    <div class="option-selected" @onclick="@OnClick" style="border: 1px solid black;">
        @if (opened)
        {
            <div class="options-wrapper" style="position: absolute; top: 30px; left: 0; border:1px solid red; background-color: white; z-index: 100;">
                Sample Content
            </div>            
        }
    </div>
</div>  

Component.razor.cs

using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace Accounting.Web.Components.Select
{
    public partial class Select
    {
        [Parameter]
        public int Id { get; set; } = default!;
        
        [Parameter]
        public RenderFragment ChildContent { get; set; } = default!;

        [Inject]
        public IJSRuntime JSRuntime { get; set; }

        private IJSObjectReference jsModule;

        public bool opened = false;


        public void OnClick()
        {
            opened = !opened;

            if (opened)
            {
                jsModule.InvokeVoidAsync("adjustPosition", "select-" + Id);                
            } 
        }

        protected override async Task OnInitializedAsync()
        {
            jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./scripts/test.js");
        }        
    }
}

test.js

export function adjustPosition(node) {
    console.log(node);
    console.log($("#" + node + " .options-wrapper").length);   // this always 0 
}

The problem is that the div (.options-wrapper) which is shown in the OnClick event is not yet available when I Invoke the JS, therefore the JS script has no access to it.

I suspect this could potentially be solved by adding a timer in the JS script, however I was wondering if there was a less hackier solution available to my problem above?

Musaffar Patel
  • 905
  • 11
  • 26
  • @Ouroborus I'm not sure, I assumed JSRuntime would be running in the client browser ... Is it possible to run JS on the browser (but call it from Blazor code)? Furthermore, my idea of adding a setTimeout to the JS code seemed to work but I want top avoid this if possible. – Musaffar Patel Nov 13 '22 at 20:58

2 Answers2

4

You should create an ElementReference object and pass it to jsModule.InvokeVoidAsync. The ElementReference object will contain a reference to the div element

<div @ref="ReferenceToDiv" id="select-@Id" style="background-color: red; width:300px; height: 300px">

 </div>

@code
{
    ElementReference ReferenceToDiv;
    // As you can see, you should call the "adjustPosition" method from the 
    // `OnAfterRenderAsync` method to ensure that the div element has been 
    // rendered. DO Not Re-render In Vain. That is, do not use
    // await Task.Delay(1); to re-render your component

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (opened)
        {
          await jsModule.InvokeVoidAsync("adjustPosition", ReferenceToDiv);                
        } 
    }

    public void OnClick()
    {
        opened = !opened;
       
    }
}

test.js

export function adjustPosition(element) {
    // Should return 300px
    console.log($(element.style.width);   
}
enet
  • 41,195
  • 5
  • 76
  • 113
  • 1
    This is precisely the type of solution I was aiming for (using Element Reference and no delays or timeouts), thanks for the solution! – Musaffar Patel Nov 13 '22 at 23:54
2

You have to wait for the rendering to update the DOM.

public /* void */ async Task OnClick()
{
    opened = !opened;
    await Task.Delay(1);  // allow the rendering to happen

    if (opened)
    {
        await jsModule.InvokeVoidAsync("adjustPosition", "select-" + Id);                
    } 
}

If that doesn't work you could chose to hide the element with hidden="@(!opened)" instead of removing it completely with the @if() { }


Ok, this may result in an extra Render. If that's unwanted, use the other answer here.

H H
  • 263,252
  • 30
  • 330
  • 514