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.
And here’s how it looks when the copy fails.
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.