Blazor JavaScript 传递 HTML 元素引用

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://blazor-university.com/javascript-interop/calling-javascript-from-dotnet/passing-html-element-references/

传递 HTML 元素引用


编写 Blazor 应用程序时,不建议操作文档对象模型 (DOM),因为它可能会干扰 其增量渲染树, 对 HTML 的任何更改都应在我们的组件内的 .NET 代码中进行管理。

有时我们可能希望继续,让JavaScript与我们生成的HTML进行交互。 实现这一点的标准 JavaScript 方法是给我们的 HTML 元素一个并让 JavaScript 找到它 用。 在静态生成的HTML页面中,这非常简单, 但是,当通过组合许多组件的输出来动态创建页面时,很难确保 ID 是 在所有组件中独一无二。 Blazor 使用元素标记和结构解决了此问题。iddocument.getElementById('someId')@refElementReference

@ref和元素引用

当我们需要对 HTML 元素的引用时,我们应该使用 . 我们通过创建一个成员来识别组件中的哪个成员将保存对 HTML 元素的引用 ,并使用属性在元素上标识它。@refElementReference@ref

@page "/"

<h1 @ref=MyElementReference>Hello, world!</h1>
Welcome to your new app.

@code {
  ElementReference MyElementReference;
}
  • 第 3
    行 定义一个 HTML 元素,并用于指定在引用该元素时将使用组件中的哪个成员 (MyElementReference)。
    @ref
  • 第 7
    行 引用用 修饰的元素时将使用的成员。
    @ref

如果我们更改新 Blazor 应用程序的 Index.razor 文件以添加对该元素的元素引用,并且 运行应用程序,我们将看到类似于以下生成的 HTML 的内容。h1

<h1 \_bl\_bc0f34fa-16bd-4687-a8eb-9e3838b5170d="">Hello, world!</h1>

添加此特殊格式的属性是 Blazor 唯一标识元素的方式 而无需劫持元素的参数。 我们现在将使用 、 和 JavaScript 互操作来解决一个常见问题。id@refElementReference

案例:自动对焦元件

HTML 规范具有可应用于任何可聚焦元素的属性; 加载页面时,浏览器将找到第一个装饰有的元素并给予其焦点。 由于 Blazor 应用并没有真正导航(HTML 只是重写并更改了浏览器 URL), 当我们导航到新 URL 并向用户呈现新内容时,浏览器不会扫描属性。 这意味着将属性放在输入上不起作用。 这就是我们将使用 JavaScript 互操作、 和 .autofocusautofocusautofocusautofocus@refElementReference

观察自动对焦问题

  • 首先创建一个新的 Blazor 应用程序。
  • 在每个页面中,将内容替换为每个指令下方的相同标记。@page

输入您的姓名:

运行应用程序并观察元素如何不自动获得焦点,即使在第一页加载时也是如此。<input>

解决自动对焦问题

  • 在 wwwroot 文件夹中创建一个脚本文件夹。
  • 在该文件夹中创建一个名为“自动对焦”的新文件.js并输入以下脚本。
var BlazorUniversity = BlazorUniversity || {};
BlazorUniversity.setFocus = function (element) {
    element.focus();
};

确保在 /Pages/_Host.cshtml(服务器端 Blazor 应用)或 /wwwroot/index.html(WebAssembly Blazor 应用)中添加对此脚本的引用。

在 Index.razor 页面中,按如下所示更改标记:

@page "/"
@inject IJSRuntime JSRuntime
Enter your name
<input @ref=ReferenceToInputControl />

@code
{
    ElementReference ReferenceToInputControl;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus", ReferenceToInputControl);
    }
}
  • 第 4
    行 使用装饰器为输入提供在组件中唯一的标识。
    @ref
  • 第 8 行
    这是将保存元素标识的成员,该成员的类型必须是 。
    ElementReference
  • 第 12 行
    如果这是此组件首次渲染, 元素引用被传递给我们的 JavaScript,它为元素提供了焦点。

现在,在页面之间切换应该会导致第一页上的输入在呈现该特定页面时获得焦点。

组件化我们的自动对焦解决方案

添加 JavaScript 在每个页面上设置焦点并不算什么工作,但它是重复的。 此外,根据显示的选项卡表将自动焦点设置为选项卡控件中的第一个控件将需要更多的工作。 这是我们应该以可重用的形式编写的东西。

首先,更改我们其中一个页面的标记,使其使用新的自动对焦控件。

@page "/"
Enter your name
<input @ref=ReferenceToInputControl />
<AutoFocus Control=ReferenceToInputControl/>

@code {
  ElementReference ReferenceToInputControl;
}

在 /Shared 文件夹中,创建一个名为 Autofocus.razor 的新组件,并输入以下标记。

@inject IJSRuntime JSRuntime
@code {
    [Parameter]
    public ElementReference Control { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus", Control);
    }
}
  • 第 4 行 为
    名为的组件定义一个参数,该参数接受 以标识哪个控件应接收焦点。
    ControlElementReference
  • 第 9
    行 执行我们的 JavaScript 以便将焦点设置为指定的控件。

此解决方案的问题在于传递组件参数的值 在渲染树构建过程中, 而元素引用在构建渲染树之前无效,并且 结果已在浏览器中呈现为 HTML。 此解决方案会导致错误 , 因为 的值在传递给我们的自动对焦组件时无效。element.focus is not a functionElementReference

注意:不要过早使用元素引用!

正如我们在渲染树部分看到的, 在其呈现阶段,Blazor 根本不更新浏览器 DOM。 只有在所有组件的呈现完成后,Blazor 才会比较新的和以前的呈现树,然后更新 DOM 具有尽可能少的更改。

这意味着在构建渲染树时,使用 引用的元素可能还没有 存在于浏览器 DOM 中 - 因此任何通过 JavaScript 与它们交互的尝试都将失败。 因此,我们永远不应该尝试在任何组件生命周期中使用实例 或 以外的方法, 而且由于组件的参数是在构建渲染树期间设置的,因此我们无法将 a 作为参数传递,因为它在组件的生命周期中太早了。 当然,从用户事件(如按钮单击)访问引用是可以接受的 因为页面已经生成到 HTML 中。@refElementReferenceOnAfterRenderOnAfterRenderAsyncElementReference

事实上,直到调用方法之前,才会设置实例。 Blazor 进程如下所示:ElementReferenceOnAfterRender*

  1. 为页面生成虚拟呈现树。
  2. 将更改应用于浏览器的 HTML DOM。
  3. 对于每个修饰的元素,更新 Blazor 组件中的成员。@refElementReference
  4. 执行生命周期方法。OnAfterRender*

我们可以通过更改标准 Blazor 应用程序的 Index.razor 组件来证明此过程,以便在组件生命周期的各个点将 an 序列化为字符串,并将序列化的文本呈现到屏幕。 将新项目中的 Index.razor 更改为以下标记并运行应用程序。ElementReference

@page "/"

<h1 @ref=MyElementReference>Hello, world!</h1>
<button @onclick=ButtonClicked>Show serialized reference</button>

<code><pre>@Log</pre></code>

Welcome to your new app.

@code {
    string Log;

    ElementReference MyElementReference;

    protected override void OnInitialized()
    {
        Log += "OnInitialized: ";
        ShowSerializedReference();
    }

    protected override void OnAfterRender(bool firstRender)
    {
        Log += "OnAfterRender: ";
        ShowSerializedReference();
    }

    private void ButtonClicked()
    {
        Log += "Button clicked: ";
        ShowSerializedReference();
    }

    private void ShowSerializedReference()
    {
        Log += System.Text.Json.JsonSerializer.Serialize(MyElementReference) + "\\r\\n";
    }
}
  1. 我们的组件实例已创建。 (第 15 行)被执行。OnInitialized
  2. 的值被序列化为我们的字符串(第 33 行)。MyElementReferenceLog
  3. 渲染树生成。
  4. 浏览器的 DOM 已更新
  5. Blazor 检查用它们标识的元素进行修饰并更新这些元素。@refElementReference
  6. OnAfterRender在我们的组件(第 21 行)上执行。
  7. 的值被序列化为我们的字符串,但不显示 - 我们必须调用才能看到它,但 的值更新。MyElementReferenceLogStateHasChangedLog
  8. 用户单击该按钮。
  9. 的值被序列化为我们的字符串。MyElementReferenceLog
  10. Blazor 执行以响应按钮单击。StateHasChanged
  11. 我们在屏幕上看到更新的,以显示从步骤 #7 和 #9 添加的值 - 这两个都显示非空标识符。Log

完成自动对焦组件

我们可以传入一个, 然后,我们的自动对焦组件可以在其生命周期方法中执行此操作 - 此时返回的值将有效。Func<ElementReference>FuncOnAfterRender*

更改自动对焦控件以接受 ,并且为了获得良好的度量,请确保设置的值不为 null。Func

@inject IJSRuntime JSRuntime
@code {
    [Parameter]
    public Func<ElementReference> GetControl { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (GetControl is null)
            throw new ArgumentNullException(nameof(GetControl));

        if (firstRender)
            await JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus", GetControl());
    }
}

现在可以按如下方式使用该组件:

@page "/"
Enter your name
<input @ref=ReferenceToInputControl />
<AutoFocus GetControl=@( () => ReferenceToInputControl)/>

@code {
  ElementReference ReferenceToInputControl;
}

有计划让未来的 Blazor 自动创建成员。ElementReference