Blazor Server app - JS Interop call within a reusable component not working when multiple components on screen

198 views Asked by At

I have a javascript event I need to listen for (kicked off by an outside vendor library) and then act on that inside of my component by hiding some content when that event is fired. I've gotten it working when the component only appears on a given page once, but the first instance of the component doesn't work when the component is included on the page more than once.

The component code:

...

// this is a method that is called internally by the component when needed but also externally 
// when the javascript event is fired by the 3rd party library that controls an input box on 
// the screen that has an "x" icon in it that clears out its contents.  This code also clears 
// out the resulting list of data that came back when the user typed something into it
[JSInvokable("CancelAutocomplete")]
public async Task CancelAutocomplete()
{
    this.IsLoading = false;
    this.SearchResults = null;
    await InvokeAsync(StateHasChanged);
}

// this i found online to combat the need for a static method when invoking blazor code from js
// see the next code block down to see what this looks like on the JS side
private DotNetObjectReference<PersonAutocompleteInput>? dotNetHelper;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        dotNetHelper = DotNetObjectReference.Create(this);
        await JS.InvokeVoidAsync("Helpers.setDotNetHelper", dotNetHelper);
    }
}

/// <summary>
/// Disposes of the dotNetHelper so its not gobbling memory
/// </summary>
protected override void Dispose(bool disposed)
{
    base.Dispose(disposed);
    this.dotNetHelper?.Dispose();
}

Then there is the javascript code (currently called from my MainLayout.razor.js so it would be available on any page that might use the above component):

...

class Helpers {
    static dotNetHelper;

    static setDotNetHelper(value) {
        Helpers.dotNetHelper = value;
    }

    static async cancelAutocomplete() {
        await Helpers.dotNetHelper.invokeMethodAsync('CancelAutocomplete');
    }

}

window.Helpers = Helpers;

In the same javascript file is the code that actually listens for the custom javascript event:

// our 3rd party library throws the 'custom-clear-click' event when a user presses the 'x' icon in a user input box to clear the text
// when that happens we should clear out the autocomplete box as well
document.addEventListener('custom-clear-click', (evt) => {

    Helpers.cancelAutocomplete();

});

In the razor component itself there is code that shows or hides based on whether there are results in SearchResults:

@if (this.SearchResults != null)
{
    ... html stuff here
}

To summarize, this all works when the component only appears once, however if it appears twice on the page nothing happens for the first component, it only works on the second component. No errors thrown in the console.

1

There are 1 answers

0
erikrunia On BEST ANSWER

After some discussion here and further research here

My final solution works and looks like this:

MainLayout.razor.js

// third party library throws the 'custom-clear-click' event when a user presses the 'x' icon in a user input box to clear the text
// when that happens we should clear out the autocomplete box as well
document.addEventListener('twc-clear-click', (evt) => {

    interopCall(evt.target);

});

// this allows us to independently track all of our XXXAutocompleteInput components so we can close the autocomplete results windows on the X click event from third party library
// now the appropriate autocomplete results are closed no matter where it is clicked from or how many are on the screen at once

// this is where we store the unique dotnethelper with the input element from each component
window.setup = (element, dotNetHelper) => {
    element.dotNetHelper = dotNetHelper;
}

// this is what we call from our javascript event thats emitted by third party library and pass in the element it was called from and then call the correct dotNetHelper
// each autocomplete razor component has its own CancelAutocomplete event, which is called below via the appropriate element and dotNetHelper
window.interopCall = async (element) => {
    await element.dotNetHelper.invokeMethodAsync('CancelAutocomplete');
}

Component .razor code

...

// gets a reference to the input box for use in the javascript interop calls when the X button on the TDS input throws the twc-clear-click event
private ElementReference inputRef;

...

[JSInvokable("CancelAutocomplete")]
public async Task CancelAutocomplete()
{
    try
    {
        // do stuff here that changes state
        await InvokeAsync(StateHasChanged);
    } catch(Exception e)
    {

    }
}

// this stores a reference to this component (objRef) AND a the specific instance of an input element contained within it (inputRef) 
// and passes it to MainLayout.razor.js which stores it globally by objRef and inputRef for reference later
private DotNetObjectReference<PersonAutocompleteInput>? objRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        objRef = DotNetObjectReference.Create(this);
        await JS.InvokeVoidAsync("setup", inputRef, objRef);
    }
}

/// <summary>
/// Disposes of the timer so its not gobbling memory
/// </summary>
protected override void Dispose(bool disposed)
{
    base.Dispose(disposed);
    this.objRef?.Dispose();
}

Component .razor html

...

<input @ref="inputRef" ...>...</input>

...