📑

Blazor で対象要素のスクロールバーを一番下まで移動させるコンポーネントを作る

2024/09/17に公開

Blazor の InteractiveServer の画面で、対象要素のスクロールバーを一番下までスクロールさせる必要があったので作りました。忘れないようにメモ。

スクロール処理自体は JavaScript で書いて、それを Razor コンポーネントから必要に応じて呼び出すようにしています。必要に応じて呼び出すために、ScrollToBottomContext というクラスを作って、そのクラスのインスタンスを Razor コンポーネントに渡して、スクロール処理をリクエストするようにしています。

ScrollToBottom.razor
@using BlazorApp1.Utilities
@implements IAsyncDisposable
@inject IJSRuntime JS

@code {
    [Parameter]
    [EditorRequired]
    public ScrollToBottomContext Context { get; set; } = null!;

    [Parameter]
    [EditorRequired]
    public string ScrollHostId { get; set; } = null!;

    private string _scrollHostId = null!;

    private IJSObjectReference? _scrollsModule;
    private IJSObjectReference? _scrollHost;

    protected override void OnParametersSet()
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(ScrollHostId);

        if (string.IsNullOrEmpty(_scrollHostId))
        {
            _scrollHostId = ScrollHostId;
        }
        else if (_scrollHostId != ScrollHostId)
        {
            throw new ArgumentException("ScrollHostId must not be changed.", nameof(ScrollHostId));
        }
    }

    protected override bool ShouldRender() => Context.IsRequestScrollToBottom;

    private async ValueTask ScrollToBottomAsync()
    {
        if (_scrollsModule == null || _scrollHost == null) return;
        await _scrollsModule.InvokeVoidAsync("scrollToBottom", _scrollHost);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _scrollsModule = await JS.InvokeAsync<IJSObjectReference>("import", "./scripts/scrolls.js");
            _scrollHost = await JS.InvokeAsync<IJSObjectReference>("document.getElementById", ScrollHostId);
        }
        else
        {
            if (Context.IsRequestScrollToBottom)
            {
                await ScrollToBottomAsync();
                Context.Reset();
            }
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (_scrollsModule != null)
        {
            await _scrollsModule.DisposeAsync();
        }

        if (_scrollHost != null)
        {
            await _scrollHost.DisposeAsync();
        }
    }

}
scrolls.js
export function scrollToBottom(scrollHost) {
    scrollHost.scrollTop = scrollHost.scrollHeight;
}
ScrollToBottomContext.cs
namespace BlazorApp1.Utilities;

public class ScrollToBottomContext
{
    public bool IsRequestScrollToBottom { get; private set; }

    public void RequestScrollToBottom()
    {
        IsRequestScrollToBottom = true;
    }

    public void Reset()
    {
        IsRequestScrollToBottom = false;
    }
}

使い方

ScrollToBottom.razor を使いたい Razor コンポーネントに以下のように書きます。
ScrollToBottomContext クラスの RequestScrollToBottom メソッドを呼ぶと、指定した要素のスクロールバーが一番下まで移動します。
実際のスクロール処理は ScrollToBottom コンポーネントの OnAfterRenderAsync メソッドで行っているので、RequestScrollToBottom メソッドを呼ぶと、次のレンダリング時にスクロール処理が行われます。ボタンクリック処理の場合はボタンのクリック処理が終わると再レンダリングが行われるので、スクロールがされることになります。ボタンクリックイベントなどのような自動的に再レンダリングが行われないような場所で RequestScrollToBottom メソッドを呼う場合は、StateHasChanged メソッドを呼んで再レンダリングを行う必要があります。

<button @onclick="SomeButtonClick">これを押したら下にスクロール</button>

<ScrollToBottom Context="_scrollToBottomContext" ScrollHostId="scrollHost">
<div id="scrollHost" style="height: 200px; overflow-y:auto;">
    何かスクロールさせたいコンテンツ
</div>

@code {
    private readonly ScrollToBottomContext _scrollToBottomContext = new();

    public void SomeButtonClick()
    {
        _scrollToBottomContext.RequestScrollToBottom();
    }
}

ということでメモでした。

Microsoft (有志)

Discussion