Copy to Clipboard component for Blazor

copy to clipboard

In this new post, I will create a Copy to Clipboard component for Blazor. I use the button to notify if the copy is successful. Then, I return the button to its original state.

Here’s how the app looks when it works correctly.

The behaviour of the component when it can copy the text in the clipboard- Copy to Clipboard component for Blazor
The behaviour of the component when it can copy the text in the clipboard

And here’s how it looks when the copy fails.

 The behaviour of the component when it can't copy the text in the clipboard - Copy to Clipboard component for Blazor
The behaviour of the component when it can’t copy the text in the clipboard

We’ll build a component that allows users to copy and paste text from a markdown previewer. This process involves three steps:

  • Implement a ClipboardService
  • Create a shared CopyToClipboardButton component
  • Use the component with a markdown previewer

The ful source code is available on GitHub..

Implement a ClipboardService

So, to write text to the clipboard, we’ll need to use a browser API. This work involves some quick JavaScript, whether from a pre-built component or some JavaScript interoperability. Luckily for us, we can create a basic ClipboardService that allows us to use IJsRuntime to call the Clipboard API, which is widely used in today’s browsers.

Then, we’ll create a WriteTextAsync method that takes in the text to copy. Then, we’ll write the text to the API with a navigator.clipboard.writeText call. Here’s the code for Services/ClipboardService.cs:

using Microsoft.JSInterop;

namespace PSC.Blazor.Components.CopyToClipboard
{
    public class ClipboardService
    {
        private readonly Lazy<Task<IJSObjectReference>> moduleTask;
        private readonly IJSRuntime _jsRuntime;

        public ClipboardService(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        public ValueTask WriteTextAsync(string text)
        {
            return _jsRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
        }
    }
}

Then, in Program.cs, reference the new service we created:

builder.Services.AddScoped<ClipboardService>();

With that out of the way, let’s create the CopyToClipboardButton component.

Create a shared CopyToClipboardButton component

So, at the top of the file, let’s inject our ClipboardService. (We won’t need a @page directive since this will be a shared component and not a routable page.)

@inject ClipboardService ClipboardService

Now, we’ll need to understand how the button will look. For both the active and notification states, we need to have the following:

  • Message to display
  • Font Awesome icon to display
  • Bootstrap button class

With that in mind, let’s define all those at the beginning of the component’s @code block.

@code {
    [Parameter] public string Id { get; set; } = "CopyToClipboard-" + Guid.NewGuid().ToString();
    [Parameter] public string SuccessButtonClass { get; set; } = "btn btn-success";

    [Parameter] public string InfoButtonClass { get; set; } = "btn btn-info";
    [Parameter] public string ErrorButtonClass { get; set; } = "btn btn-danger";

    [Parameter] public string CopyToClipboardText { get; set; } = "Copy to clipboard";
    [Parameter] public string CopiedToClipboardText { get; set; } = "Copied to clipboard!";
    [Parameter] public string ErrorText { get; set; } = "Oops. Try again.";

    [Parameter] public string FontAwesomeCopyClass { get; set; } = "fa fa-clipboard";
    [Parameter] public string FontAwesomeCopiedClass { get; set; } = "fa fa-check";
    [Parameter] public string FontAwesomeErrorClass { get; set; } = "fa fa-exclamation-circle";

    [Parameter] public string Text { get; set; }
}

With that, we need to include a Text property as a component parameter. The caller will provide this to us, so we know what to copy.

[Parameter] 
public string Text { get; set; }

Now, using for C# 9 records and target typing, we can create an immutable object to work with the initial state.

record ButtonData(bool IsDisabled, string ButtonText, string ButtonClass, string FontAwesomeClass);

ButtonData buttonData = new(false, CopyToClipboardText, InfoButtonClass, FontAwesomeCopyClass);

Now, in the markup, we can add a new button with the properties we defined.

<button class="@buttonData.ButtonClass" disabled="@buttonData.IsDisabled" 
        @onclick="CopyToClipboard">
    <i class="@buttonData.FontAwesomeClass"></i> @buttonData.ButtonText
</button> 

You’ll get an error because your editor doesn’t know about the CopyToClipboard method. Let’s create it. First, set up an originalData variable that holds the original state, so we have it when it changes.

var originalData = buttonData;

Now, we’ll do the following in a try/catch block:

  • Write the text to the clipboard
  • Update buttonData to show it was a success/failure
  • Call StateHasChanged
  • Wait 1500 milliseconds
  • Return buttonData to its original state

We need to explicitly call StateHasChanged to notify the component it needs to re-render because the state … has changed.

Here’s the full CopyToClipboard method (along with a TriggerButtonState private method for reusability).

public async Task ToClipboard()
{
    var originalData = buttonData;
    try
    {
        await ClipboardService.WriteTextAsync(Text);

        buttonData = new ButtonData(true, CopiedToClipboardText, 
            SuccessButtonClass, FontAwesomeCopiedClass);
        await TriggerButtonState();
        buttonData = originalData;
    }
    catch
    {
        buttonData = new ButtonData(true, ErrorText, ErrorButtonClass, FontAwesomeErrorClass);
        await TriggerButtonState();
        buttonData = originalData;
    }
}

private async Task TriggerButtonState()
{
    StateHasChanged();
    await Task.Delay(TimeSpan.FromMilliseconds(1500));
}

For reference, here’s the entire CopyToClipboardButton component:

@inject ClipboardService ClipboardService

<button class="@buttonData.ButtonClass" disabled="@buttonData.IsDisabled" 
        @onclick="ToClipboard" id="@Id">
    <i class="@buttonData.FontAwesomeClass"></i> @buttonData.ButtonText
</button>

@code {
    [Parameter] public string Id { get; set; } = "CopyToClipboard-" + Guid.NewGuid().ToString();
    [Parameter] public string SuccessButtonClass { get; set; } = "btn btn-success";

    [Parameter] public string InfoButtonClass { get; set; } = "btn btn-info";
    [Parameter] public string ErrorButtonClass { get; set; } = "btn btn-danger";

    [Parameter] public string CopyToClipboardText { get; set; } = "Copy to clipboard";
    [Parameter] public string CopiedToClipboardText { get; set; } = "Copied to clipboard!";
    [Parameter] public string ErrorText { get; set; } = "Oops. Try again.";

    [Parameter] public string FontAwesomeCopyClass { get; set; } = "fa fa-clipboard";
    [Parameter] public string FontAwesomeCopiedClass { get; set; } = "fa fa-check";
    [Parameter] public string FontAwesomeErrorClass { get; set; } = "fa fa-exclamation-circle";

    [Parameter] public string Text { get; set; }

    record ButtonData(bool IsDisabled, string ButtonText, string ButtonClass, 
            string FontAwesomeClass);

    ButtonData buttonData;

    protected override void OnInitialized()
    {
        buttonData = new(false, CopyToClipboardText, InfoButtonClass, 
            FontAwesomeCopyClass);
        base.OnInitialized();
    }

    public async Task ToClipboard()
    {
        var originalData = buttonData;
        try
        {
            await ClipboardService.WriteTextAsync(Text);

            buttonData = new ButtonData(true, CopiedToClipboardText, 
                SuccessButtonClass, FontAwesomeCopiedClass);
            await TriggerButtonState();
            buttonData = originalData;
        }
        catch
        {
            buttonData = new ButtonData(true, ErrorText, ErrorButtonClass, 
                FontAwesomeErrorClass);
            await TriggerButtonState();
            buttonData = originalData;
        }
    }

    private async Task TriggerButtonState()
    {
        StateHasChanged();
        await Task.Delay(TimeSpan.FromMilliseconds(1500));
    }
}

Great! You should now be able to see the button in action.

Use the component with a markdown previewer

So, now we can build a page to use the component. First, install the component from NuGet and then add it in the _Imports.razor

@using PSC.Blazor.Components.CopyToClipboard

We can now build a simple with a simple TextArea.

Now, the page contains the following code

@page "/"

<CopyToClipboardButton Text="@Body" />

<div class="row">
    <div class="col-6" height="100">
        <textarea class="form-control" 
        @bind-value="Body" 
        @bind-value:event="oninput"></textarea>
    </div>
</div>

@code {
    public string Body { get; set; } = string.Empty;
}

So, I’m adding a textarea, binding to the Body text. That’s really all there is to it!

Wrap up

In this post, we built a reusable CopyToClipboard component for Blazor to copy text to the clipboard. As a bonus, the component toggles between active and notification states.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.