How to create Blazor components with custom JavaScript and CSS in the components library and properly consume them from WebAssembly application

Spread the love

A few days ago, the Blazor WebAssembly was officially released. And It’s a good reason to “taste” it. After watching a great “Modern Web UI with Blazor WebAssembly” demo on Microsoft Build 2020, a couple of ideas of cool Blazor components appeared in my mind. But such components should interact with some existing JavaScript libraries, so I had to understand how to develop them properly.

Of course, there are excellent JavaScript articles in the Blazor documentation, but they didn’t give me a clear understanding of how to perform the interaction between the C# code of component consumer and JavaScript code from Blazor component library.

And It’s an interesting subject to investigate. So, let’s do it together.

Creating projects

We have to create two separate projects

  • CoolComponentsLib. It’s an ASP.NET Core Razor components class library project where the component will be developed
  • ConsumerApp. It’s a Blazor WebAssembly application project which will consume the component from the library.

I will use the Visual Studio Code and .NET Core CLI. You also can use Visual Studio Community or another IDE.

Creating ASP.NET Core Razor components class library

It’s pretty simple and is described in detail in the official ASP.NET Core documentation.
Just put this command in your Visual Studio Code Terminal Window

dotnet new razorclasslib -o CoolComponents

NET Core CLI should create a new project with a predefined Blazor component and static content. You can see what we should have on the image below.

Creating ASP.NET Core Blazor WebAssembly application project

Just put the next command into your console

dotnet new blazorwasm -o ConsumerApp

.NET Core CLI should create a new Blazor WebAssembly project

Also we want to use our components project as reference

cd ConsumerApp
dotnet add reference ../CoolComponents

Editing Blazor component in the CoolComponents library

First, let’s rename Component1.razor to BigGreenButtonClicker.razor. Probably you guess it will contain the big grin button which should be clicked immediately :). Moreover, I want to use some JavaScript code which should be run asynchronously after the button was clicked. And this JavaScript code should return some value that should be displayed in the component.

So, change the code inside BigGreenButtonClicker.razor

<div class="my-component">
    <div class="my-buttonkeeper">
        <button class="my-button" @onclick="OnClickCallback">@Title</button>
    </div>
</div>
<div>Your answer: @ResultAnswer</div>
@code
{
    [Parameter]
    public string Title { get; set; }

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

    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}

Here you can see quite simple HTML markup with few <div> elements and one <button>. The text inside the button is defined in the @Title parameter of the component. And the @onclick event of the button should be processed by OnClickCallback event callback. The code of the callback will be defined outside of the component.

Also the @ResultAnswer parameter is defined. It will contain some value filled in outside of component.

The HTML markup uses three CSS classes which are declared in the styles.css file inside wwwroot directory.

.my-component {
    border: 2px dashed red;
    padding: 1em;
    margin: 1em 0;
    background-image: url('background.png');
}

.my-buttonkeeper{
    text-align: center;
}

.my-button {
    width: 150px;
    height: 150px;
    background-color: green;
    color: white;
    font-size: 20px;
}

But what about JavaScript? Lets’s find what we have.

Inside wwwroot directory we have ready to use exampleJsInterop.js file which was created by CLI. Let’s modify it slightly.

window.exampleJsFunctions = {
  showPrompt: function (message) {
    return prompt(message);
  }
};

But where this code should be called? Let’s look inside ExampleJsInterop.cs.

...
    public class ExampleJsInterop
    {
        public static ValueTask<string> Prompt(IJSRuntime jsRuntime, string message)
        {
            // Implemented in exampleJsInterop.js
            return jsRuntime.InvokeAsync<string>(
                "exampleJsFunctions.showPrompt",
                message);
        }
    }
...

And yes, it’s here. So, we have the C# class with static method which can be used outside the component.

Looks like our component is ready. Just move inside CoolComponents folder and build the project to be sure everything is OK.

cd CoolComponents
dotnet build

Using the BigGreenButtonClicker component in the ConsumerApp application

I will not perform the application cleanup this time. You can do this in a similar way as described in my Using three.js with ASP.NET Core Blazor Server application post.

I’m going to use the existing Index.razor page to host our cool big button. So, let’s modify the Index.razor file inside the Pages directory

@page "/"
@using CoolComponents
@inject IJSRuntime JS
<div>
<BigGreenButtonClicker 
  Title="Click me and be happy!"
  OnClickCallback="OnClick" 
  ResultAnswer="@resultAnswer">
</BigGreenButtonClicker>
</div>
@code
{
  string happy = ":-)";
  string sad = ":-(";
  string resultAnswer = string.Empty;

  async void OnClick(MouseEventArgs arg)
  {
    var task = ExampleJsInterop.Prompt(JS, "Give the answer to be happy!");
    resultAnswer = await task;
    var smile = string.IsNullOrWhiteSpace(resultAnswer) ? sad : happy;
    resultAnswer = $"{resultAnswer} {smile}";
  }
}

Here we’re using the CoolComponents namespace where our cool green button is declared.

Then we use our component in the HTML markup and bind all it’s parameters to the properties which are declared inside our C# code. Also we have to bind the event callback to the OnClick() method.

Inside the OnClick() method call the JavaScriptInterop.Promt() method which is also defined inside CoolComponents class library.

Before running our application we have to do yet another change. This time inside index.html file inside ConsumerApp/wwwroot directory. This file is used as a master page or a template for all pages of our application. So, here is a place where all static content should be loaded.

Our CoolComponents library also has some static content files, so let’s load them here

<!DOCTYPE html>
<html>
<head>
    ...
    <link href="_content/CoolComponents/styles.css" rel="stylesheet" />
</head>
<body>
    ...
    <script src="_content/CoolComponents/exampleJsInterop.js"></script>
</body>
</html>

Looks like our app is ready, so let’s try it

cd ../ConsumerApp
dotnet run

It looks like everything is OK. The page has a big green button. The prompt is displayed after the button clicked. But wait. Where is the answer after the user has pressed the OK button on the prompt dialog?

The debugging shows everything works fine

But the answer will be displayed only if we click the green button again, then if we put the new answer and click the OK button in the prompt dialog. And looks like it’s a previously entered answer!

It’s a bug and it have to be fixed.

Fixing the bugs

The root cause of the bug is the asynchronous behavior of JavaScript interop. The JavaScript code is called after the page has been rendered. And after executing the JavaScript function the page doesn’t know the state of it’s properties have been changed. We have to notify it about this.

To do this, modify the Index.razor page in the next way:

@code
{
  EditContext editContext;
  ...
  async void OnClick(MouseEventArgs arg)
  {
   ...
    resultAnswer = $"{resultAnswer} {smile}";
    editContext.NotifyFieldChanged(editContext.Field(nameof(resultAnswer)));
  }

  protected override void OnInitialized()
  {
    base.OnInitialized();
    editContext = new EditContext(this);
    editContext.OnFieldChanged += (sender, args) => StateHasChanged();
  }
}

Here we are using instance of the EditContext which is responsible for notifying the page about it’s fields change.

And now everything should work fine 🙂

You can find the complete source code of this investigation in my BlazorLibTest repository on Github.

That’s all and happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *