Javascript Interop in Blazor web assembly - part 4 - using NPM in Blazor

...

In the previous article we moved our code into a component so we can reuse that functionality on multiple pages. Because we used javascript isolation the javascript file was imported within our C# code and our component just has to be placed wherever we want it. Nothing needs to be done with the js file. This is already pretty powerful, but it is somewhat limited.

As we discussed at the close of that post, one of the benefits of being able to call out to javascript is that we can take advantage of it's vast ecosystem of already existing libraries. The difficulty here is that many libraries have dependencies on other libraries. This can become quite tricky to manage, so we'll use NPM to take care of it for us. NPM stands for Node Packager Manager, and as it's name suggests, it manages packages. It's somewhat similar to nuget.

It comes bundled with Nodejs. Node is the runtime and npm is the package manager for it. However, we can still use it even if we aren't developing a node app. I'm going to assume you've already installed Node - if not click here to download it

So our first stage is to create a new folder to hold our javascript files. We already have a js folder in our wwwroot but this is our final destination folder. We need a new folder which will hold the files we get from npm and our original source files. We'll then do some magic on them and output the result into the wwwroot/js folder. It will become clearer as we progress. So lets create another folder, we'll call it sourceJs.

right click the folder and open it in a new terminal.

Type npm init -y

This creates a new file - package.json

 

This file contains the default NPM configuration. 

We are going to install three libraries via NPM. These are videojs, which is the video player I was wanting to wrap. The other two are plugins for it that add playlist functionality.

So type 'npm install video.js videojs-playlist videojs-playlist-ui --save' in the terminal.

If you have 'Show All Files' enabled in the solution Explorer window then you'll see that under our sourceJs folder there is now a hidden node_modules folder which contains the packages we just installed and their dependencies.

 

If we expand the video.js folder then within it is a dist folder and the video.js file that we are ultimately interested in. Unfortunately it's not as simple as just copying that file to our wwwroot folder. First we need to use a bundler to work some magic for us. There are several options. Webpack is perhaps the most established and there is an article by Brian Lagunas detailing an approach using Webpack here, but we're going to use Snowpack. Its lightweight and does what we need, quickly and easily. (Plus when I tried to use webpack on my project I had a bunch of issues whereas Snowpack just worked straight away). In order to use it we have to install it via npm:

$ npm install --save-dev snowpack

an additional option to install is the Optimize plugin which will minify the files

$ npm install --save-dev @snowpack/plugin-optimize

We need to create a configuration file to tell snowpack what to do, so Add a new file in our sourceJs folder called 'snowpack.config.js' and add the following

module.exports = {
    plugins: [
        ['@snowpack/plugin-optimize']
    ],
 
    buildOptions: {
        out: '../wwwroot/js/',
        clean: true
    },
 
    mount: {
        'src''/'
    },
};

So it's telling it to output it's build to our wwwroot/js folder and to clean it first.  We next need to create a src folder in which we put our source files that we want snowpack to process. Snowpack is going to map the contents of the src folder to our wwwroot/js folder.

The final stage of configuring snowpack is to modify our project file to get visual studios build process to trigger snowpack every time we run a build. Double click the project in solution explorer and add the following to the .csproj file

  <Target Name="PreBuild" BeforeTargets="PreBuildEvent">
    <Exec  Command="npm run build"  WorkingDirectory="sourceJS" />
  </Target>

 

Now create an index.js in our src folder so there is something for snowpack to pick up.

//we import the npm packages here
// and export what we want to make public below
 
 
import videoJs from 'video.js';
import videojsPlaylist from 'videojs-playlist';
import playlistUi from 'videojs-playlist-ui';
 
let player = {};
 
 
export function initialisePlayer(id) {
    var options = {};
 
    player = videoJs(id);
 
    return player;
 
}
 
 
export function setPlaylist(player) {
 
 
    player.playlist([{
        sources: [{
            src: 'http://media.w3.org/2010/05/sintel/trailer.mp4',
            type: 'video/mp4'
        }],
        poster: 'http://media.w3.org/2010/05/sintel/poster.png'
    }, {
        sources: [{
            src: 'http://media.w3.org/2010/05/bunny/trailer.mp4',
            type: 'video/mp4'
        }],
        poster: 'http://media.w3.org/2010/05/bunny/poster.png'
    }, {
        sources: [{
            src: 'http://vjs.zencdn.net/v/oceans.mp4',
            type: 'video/mp4'
        }],
        poster: 'http://www.videojs.com/img/poster.jpg'
    }, {
        sources: [{
            src: 'http://media.w3.org/2010/05/bunny/movie.mp4',
            type: 'video/mp4'
        }],
        poster: 'http://media.w3.org/2010/05/bunny/poster.png'
    }, {
        sources: [{
            src: 'http://media.w3.org/2010/05/video/movie_300.mp4',
            type: 'video/mp4'
        }],
        poster: 'http://media.w3.org/2010/05/video/poster.png'
    }]);
 
    // Initialize the playlist-ui plugin with no option (i.e. the defaults).
    player.playlistUi();
    player.playlist.autoadvance(0);
    player.play();
 
 
}

Back in article 2 we looked at javascript modules and created one by exporting a function. Here we are exporting two functions on this file, making it a module with two public methods. The player variable is only available internally to the module so is effectively private. At the top of the file we are importing modules from the three npm packages we installed.

That file looks like we should be able to just drop it into our js folder and use it with blazors javascript interop. But no. That wouldn't work. Those packages we imported are npm packages. The 'n' standing for Node. These are designed to run on NodeJs server apps, which use a slightly different specification to browsers. Thankfully Snowpack takes care of this for us, transpiling the code in our dependencies and bundling it all up and dropping it into the wwwroot/js folder we specified as our output directory. And thanks to the build event we created it will happen as soon as we build. So press F6(Build) and we'll see it has done just that 

It's copied across our index.js file and created a snowpack folder in which its copied transpiled versions of the dependencies.

You'll remember that we already had an index.js file from the previous article, but that has been overwritten. Something to be aware of, is that it wasn't overwritten because we copied a new version of the file over. The entire contents of the folder get cleaned. If we had any other files in there, they would now be gone. So if you do this on another project and have existing files, move them into the src folder so snowpack will copy them all.

So that's the tricky part done. We have imported some libraries using NPM. We have bundled them using snowpack. We've added s step so this occurs whenever we build our code. And we have placed the output in our wwwroot/js folder. We now have an index.js file that is a javascript module just as we did in the last article. The steps to use it with Blazors javascript isolation are the same.  

We'll now change our component so that it will display our video player. Replace the contents of MyJavascriptComponent.razor with

<h3>MyJavascriptComponent</h3>
 
<div class=" container">
	<div class="row" style="height: @(Height); width: @(Width);">
 
		<video id="videoPlayer"
			   class="video-js vjs-theme-city col-sm-9"
			   controls>
		</video>
 
		<div class="col-sm-3 " style="height: @(PlaylistHeight);">
			<h4>@PlaylistTitle</h4>
			<div class="vjs-playlist ">
				<!--
				The contents of this element will be filled based on the
				currently loaded playlist
				-->
			</div>
		</div>
	</div>
</div>
@code
{
 
	[Injectpublic IJSRuntime JSRuntime { getset; }
 
	[Parameterpublic string Height { getset; } = "500px";
	[Parameterpublic string Width { getset; } = "800px";
	[Parameterpublic string PlaylistTitle { getset; } = "Playlist";
 
	public string PlaylistHeight { getset; } = "450px";
 
	protected async override Task OnAfterRenderAsync(bool firstRender)
	{
		if (firstRender)
		{
			var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
							"import""/js/index.js");
 
			var player = await module.InvokeAsync<IJSObjectReference>("initialisePlayer""videoPlayer");
 
			await module.InvokeVoidAsync("setPlaylist"player);
		}
		base.OnAfterRender(firstRender);
	}
 
}

 

I won't go into much detail on this. The html part is a very cut down version of the component I've created. Then I've put the bare minimum of code into the OnAfterRenderAsync function to get it working. We're just doing this to prove that importing the npm packages work, not go into depth on videojs and component creation or other topics. The important thing to note is that we have named the video element in the html section as 'videoPlayer' and we pass that id into the javascript code which then searches the DOM for that element and created the video.js player accordingly. The playlist plugin (in it's default option) looks for a div with the class of 'vjs-playlist'.

Within our code we have a function to initialise the player - strangely enough called 'InitialisePlayer'. Within that we import our Javascript module as before, then we call initialisePlayer on that module, receiving a reference to the player. then we call another function passing back the reference to that player. So our .NET code is calling to a javascript function. It's receiving a javascript object. It's storing a reference to that object. Then we call another javascript function and pass our .NET variable which is actually a reference to a javascript object.

 

If you run the code now you'll see a video player and a playlist of videos loaded. It looks really ugly as we haven't done anything with the css. Snowpack is capable of handling css but the focus of this series of articles is Javascript interop in Blazor. So by utilising snowpack we can take advantage of the wealth of npm packages out there. In the next article we'll look at extracting our component into a Razor component library so we can reuse it across multiple applications