TutorialsEngineering

Creating a Weavy component in a Blazor server app

May 25, 2021

Note: this tutorial was written for Weavy v8, which has a different codebase than the current version of Weavy.

In this guide you will learn how to set up a reusable Weavy component in your Blazor app. This is based on the getting started guides at dotnet.microsoft.com/learn/aspnet/blazor-tutorial.

Weavy Blazor Server Demo at GitHub

blazor+weavy

Install dotnet

You will need .NET SDK installed.

Follow the instuctions for your platform at dotnet.microsoft.com/learn/aspnet/blazor-tutorial/install.

To verify that it's working, simply type dotnet in your console.

Create a Blazor App

This will create a new basic Blazor app in a new folder. If you already have an app you can simply skip this step.

dotnet new blazorserver -o BlazorApp --no-https
cd BlazorApp
dotnet watch run

This will build and launch the dev server, usually on http://localhost:5000

Add the Weavy script

The simplest way of adding the Weavy script is to add it to the <head> section in your Pages/_Host.cshtml. Replace showcase.weavycloud.com with the domain name of your own weavy server.

Pages/_Host.cshtml

<head>
  ...
  <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  <script src="https://showcase.weavycloud.com/javascript/weavy.js"></script>
</head>

Create a JS Interop

To be able to work with Weavy in javascript we need a JS Interop in Blazor. The JS interop acts as a bridge between Blazor and JS. To not expose our bridge in the window object, we will create the bridge as a js module that we will import.

Adding a JS Interop Module

Create wwwroot/weavyJsInterop.js. We just have to expose the creation of a Weavy instance. Everything after that will be acessible as a IJSObjectReference on which we may call methods and use properties.

weavyJsInterop.js

export function weavy(...options) {
    return new window.Weavy(...options);
}

Adding a JS Interop Service

We will create a service wrapped around the JS Interop and the Weavy instance. This will provide a much simpler C# syntax around Weavy, much alike the Weavy syntax used in javascript.

Create a folder Weavy for our classes.

Place a C# class called WeavyJsInterop.cs in the Weavy folder. In this file we will first import our JS Interop Module. We can then create a new Weavy instance using the JS Module and access all methods and properties of that instance.

We will delay the initialization of the module until we request the Weavy Instance, to only use it when needed.

WeavyJsInterop.cs

using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace BlazorApp.Weavy {
    public class WeavyJsInterop : IDisposable {
        private readonly IJSRuntime JS;
        private bool Initialized = false;
        private IJSObjectReference Bridge;
        private ValueTask<IJSObjectReference> WhenImport;

        // Constructor
        // This is a good place to inject any authentication service you may use to provide JWT tokens.
        public WeavyJsInterop(IJSRuntime js) {
            JS = js;
        }

        // Initialization of the JS Interop Module
        // The initialization is only done once even if you call it multiple times
        public async Task Init() {
            if (!Initialized) {
                Initialized = true;
                WhenImport = JS.InvokeAsync<IJSObjectReference>("import", "./weavyJsInterop.js");
                Bridge = await WhenImport;
            } else {
                await WhenImport;
            }
        }

        // Calling Javascript to create a new instance of Weavy via the JS Interop Module
        public async ValueTask<IJSObjectReference> Weavy(object options = null) {
            await Init();
            // Demo JWT only for showcase.weavycloud.com
            // Configure your JWT here when using your own weavy server
            var jwt = new { jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzYW1hcmEiLCJuYW1lIjoiU2FtYXJhIEthdXIiLCJleHAiOjI1MTYyMzkwMjIsImlzcyI6InN0YXRpYy1mb3ItZGVtbyIsImNsaWVudF9pZCI6IldlYXZ5RGVtbyIsImRpciI6ImNoYXQtZGVtby1kaXIiLCJlbWFpbCI6InNhbWFyYS5rYXVyQGV4YW1wbGUuY29tIiwidXNlcm5hbWUiOiJzYW1hcmEifQ.UKLmVTsyN779VY9JLTLvpVDLc32Coem_0evAkzG47kM" };
            return await Bridge.InvokeAsync<IJSObjectReference>("weavy", new object[] { jwt, options });
        }

        public void Dispose() {
            Bridge?.DisposeAsync();
        }
    }
}

Register the JS Interop Service

To be able to use the JS Interop as a sevice throughout the app, we need to register it in our Startup.cs. Add the following line in the ConfigureServices method. You also need to use the namespace.

Startup.cs

using BlazorApp.Weavy;

// ...

public void ConfigureServices(IServiceCollection services) {
    // ...
    services.AddScoped<WeavyJsInterop>();
}

 

Adding IJSObjectReference wrappers

When communicating with Javascript from your app, you'll get back references to the Javascript objects. For convenience, we will wrap weavy, spaces and apps in extended IJSObjectReference classes. This is a convenient way to expose Javascript methods and properties to C#. It's also convenient to manage the Javascript cleanup here when you want to dispose them.

First of we'll make a base class for IJSObjectReference, so we can extend it easily. Create a class file ExtendableJSObjectREference.cs in your Weavy folder

Weavy/ExtendableJSObjectReference.cs

using Microsoft.JSInterop;
using System.Threading;
using System.Threading.Tasks;

namespace BlazorApp.Weavy {
    //
    // Summary:
    //     Wrapper around a IJSObjectReference to enable extending
    public class ExtendableJSObjectReference : IJSObjectReference {
        public IJSObjectReference ObjectReference;

        // Constructed using another IJSObjectReference
        // Possibility to delay ObjectReference assignment
        public ExtendableJSObjectReference(IJSObjectReference objectReference = null) {
            ObjectReference = objectReference;
        }

        // IMPLEMENT DEFAULT
        public ValueTask DisposeAsync() {
            return ObjectReference.DisposeAsync();
        }

        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) {
            return ObjectReference.InvokeAsync<TValue>(identifier, args);
        }

        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args) {
            return ObjectReference.InvokeAsync<TValue>(identifier, cancellationToken, args);
        }
    }
}

 

Next, well create three classes for respecively WeavyReference, SpaceReference and AppReference. Create a file WeavyReference.cs in your Weavy folder. The classes will extend the ExtendableJSObjectReference and just add the methods we want to reflect from our Javascript objects. The WeavyReference class also has automatic initialization, so it don't need to construct a Weavy instance in Javascript until it's needed.

Weavy/WeavyReference.cs

using Microsoft.JSInterop;
using System.Threading.Tasks;

namespace BlazorApp.Weavy {
    //
    // Summary:
    //     Wrapped IJSObjectReference to the Weavy instance in Javascript.
    //     Adds .Space() and .Destroy() methods.
    public class WeavyReference : ExtendableJSObjectReference {
        private bool Initialized = false;
        public WeavyJsInterop WeavyService;
        public object Options;
        public ValueTask<IJSObjectReference> WhenWeavy;

        public WeavyReference(WeavyJsInterop weavyService = null, object options = null, IJSObjectReference weavy = null) : base(weavy) {
            Options = options;
            WeavyService = weavyService;
        }

        public async Task Init() {
            if(!Initialized) {
                Initialized = true;
                WhenWeavy = WeavyService.Weavy(Options);
                ObjectReference = await WhenWeavy;
            } else {
                await WhenWeavy;
            }
        }

        public async ValueTask<SpaceReference> Space(object spaceSelector = null) {
            await Init();
            return new(await ObjectReference.InvokeAsync<IJSObjectReference>("space", new object[] { spaceSelector }));
        }

        // Used for cleanup
        public async Task Destroy() {
            await ObjectReference.InvokeVoidAsync("destroy");
            await DisposeAsync();
        }
    }

    //
    // Summary:
    //     Wrapped IJSObjectReference to a Weavy Space in Javascript.
    //     Adds .App() and .Remove() methods.
    public class SpaceReference : ExtendableJSObjectReference {
        public SpaceReference(IJSObjectReference space) : base(space) { }

        public async ValueTask<AppReference> App(object appSelector = null) {
            return new(await ObjectReference.InvokeAsync<IJSObjectReference>("app", new object[] { appSelector }));
        }

        // Used for cleanup
        public async Task Remove() {
            await ObjectReference.InvokeVoidAsync("remove");
            await DisposeAsync();
        }
    }

    //
    // Summary:
    //     Wrapped IJSObjectReference to a Weavy App in Javascript.
    //     Adds .Open(), .Close(), .Toggle() and .Remove() methods()
    public class AppReference : ExtendableJSObjectReference {
        public AppReference(IJSObjectReference app) : base(app) { }

        public ValueTask Open() {
            return ObjectReference.InvokeVoidAsync("open");
        }

        public ValueTask Close() {
            return ObjectReference.InvokeVoidAsync("close");
        }

        public ValueTask Toggle() {
            return ObjectReference.InvokeVoidAsync("toggle");
        }

        // Used for cleanup
        public async Task Remove() {
            await ObjectReference.InvokeVoidAsync("remove");
            await DisposeAsync();
        }
    }
}

Now we have a nice toolbox for being able to use Weavy in components or for custom code! Just add the namespace to your _Imports.razor file to be able to use it anywhere.

_Imports.razor

// ...
@using BlazorApp.Weavy

You can now use Weavy in code like this:

@inject WeavyJsInterop WeavyService

@code {
    // ...
    var Weavy = new WeavyReference(WeavyService);
    var Space = await Weavy.Space(new { key = "my-space" });
    var FilesApp = await Space.App(new { key = "my-files", type = "files" }); 
    // ...
}

Creating Weavy components

Now that we have a nice syntax for Weavy, we can create components for simple declarative usage. You can easily create more custom components to fit your needs.

Creating a Weavy instance compomnent

First we will create a component for the weavy instance. This will be a component creating a weavy scope for it's children.

First we inject the WeavyJsInterop service in the component. We just make use of the service when creating a new WeavyReference. We map all component parameters to weavy options using attribute splatting.

We make use of the CascadingValue tag to provide the WeavyReference to any children. This means we can make use of the same weavy instance for multiple weavy apps that are placed as children of this component. 

At last we also we let the Dispose method call the .destroy() method on our WeavyReference to clean up in Javascript whenever the component gets disposed.

Create a Weavy.razor component in your Shared folder. 

Shared/Weavy.razor

@implements IDisposable
@inject WeavyJsInterop WeavyService

<CascadingValue Value="WeavyRef">
    @ChildContent
</CascadingValue>

@code{
    WeavyReference WeavyRef;

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> Options { get; set; }

    protected override void OnInitialized() {
        WeavyRef = new(WeavyService, Options);
    }

    public void Dispose() {
        WeavyRef?.Destroy();
    }
}

This component may now be used both with our without attributes.

<Weavy>
    <!-- Any children here will have access to the weavy instance using a cascading parameter -->
    ...
</Weavy>

<Weavy id="weavy-blazor" plugins="@(new { deeplinks = true })">
    <!-- Another Weavy instance with some custom options passed to javascript -->
    ...
</Weavy>

Creating a Weavy App component

The Weavy App component will make use of the weavy instance and must therefore be placed as a child of the Weavy Instance component. The weavy instance is catched by the CascadingParameter, which will match the type of the CascadingValue.

A <div> with a @ref is used to pass as a container reference in options when creating the app in Javascript.

We need to create the app in OnAfterRenderAsync to be able to use the ElementReference of the div.

Create a parameter for the space key. If you want more flexibility in more advanced scenarios, you can create a separate weavy space component as a layer in between the weavy component and the weavy app component. Most of the time it will do to just have the space key as a parameter on the weavy app.

Shared/WeavyApp.razor

@implements IDisposable

<div @ref="WeavyContainer" class="weavy-app"></div>

@code{ 
    ElementReference WeavyContainer;

    [CascadingParameter]
    protected WeavyReference Weavy { get; set; }

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

    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> Options { get; set; }

    public SpaceReference Space;
    public AppReference App;

    protected override async Task OnAfterRenderAsync(bool firstRender) {
        if (firstRender) {
            Options.Add("container", WeavyContainer);

            Space = await Weavy.Space(new { key = SpaceKey });
            App = await Space.App(Options);
        }
    }

    public void Dispose () {
        App?.Remove();
    }
}

Add styling

Add som styling for the container. Weavy always adapts it's size to the container where it's places, therefore we need to somehow specify a height for the container otherwise the height will be 0. You man also make use of display: contents; to make weavy adapt itself to any parent node of the component instead. This makes it more flexible to use the component in different layouts. Add a WeavyApp.razor.css file in the Shared folder.

Shared/WeavyApp.razor.css

.weavy-app {
    display: contents;
}

The Weavy App component is now ready for usage. Weavy requires you to have at least a space key, an app key and an app type. You must also place the component within a Weavy Instance component and also define a height somehow on the parent.

<Weavy>
    <div style="height: 44rem;">
        <WeavyApp SpaceKey="blazor-space" key="blazor-posts" type="posts" />
    </div>
</Weavy>

Usage

Now the components are set up for declarative usage of weavy! Lets create two pages for Posts and Files to demonstrate the usage.

Pages/Posts.razor

@page "/posts"

<Weavy>
    <h1>Posts</h1>
    <div style="height: 44rem;">
        <WeavyApp SpaceKey="blazor-space" key="blazor-posts" type="posts" name="Blazor Posts" />
    </div>
</Weavy>

To make use of Razor expressions for the attributes, you should make use of the @key attribute and set it to the same as the key of the app. This way, the weavy app will get properly replaced when needed.

Pages/Files.razor

@page "/files"

<h1>Files</h1>

<nav class="nav my-2">
    <button class="btn" @onclick="@(e => { FilesKey = "blazor-files-2019"; FilesName = "Blazor Files 2019"; })">2019</button>
    <button class="btn" @onclick="@(e => { FilesKey = "blazor-files-2020"; FilesName = "Blazor Files 2020"; })">2020</button>
</nav>

<Weavy>
    <div class="card" style="height: 32rem;">
        <WeavyApp SpaceKey="blazor-space" type="files" @key="FilesKey" key="@FilesKey" name="@FilesName" />
    </div>
</Weavy>

@code {
    private string FilesKey { get; set; } = "blazor-files";
    private string FilesName { get; set; }  = "Blazor Files";
}

Don't forget to add the two pages in your NavMenu.

Shared/NavMenu.razor

...

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        ...

        <li class="nav-item px-3">
            <NavLink class="nav-link" href="posts">
                <span class="oi oi-comment-square" aria-hidden="true"></span> Posts
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="files">
                <span class="oi oi-folder" aria-hidden="true"></span> Files
            </NavLink>
        </li>
    </ul>
</div>

Now you're all done and dotnet should have automatically have recompiled everything for you! Try it out in the browser!

Learn more about Weavy Client SDK

 

Weavy

Share this post