Embedding Existing JavaScript Components Into Uno-WASM - Part 2
Let's create an app to integrate a Syntax Highlighter named PrismJS
. This library is simple and is self-contained - there's no external dependencies.
Integration of PrismJS in a project
0. Before starting
📝 To reproduce the code in this article, you must prepare a development environment using Uno's Getting Started article.
1. Create the projects
🎯 This section is very similar to the Create a Counter App with Uno Platform tutorial in the official documentation.
Start Visual Studio 2019
Click
Create a new project
Search for "Uno" and pick
Uno Platform App
.Select it and click
Next
.Give a project name and folder as you wish. It will be named
PrismJsDemo
here.Click
Create
button.Right-click on the solution and pick
Manage NuGet Packages for Solution...
Update to latest version of
Uno
dependencies. DO NOT UPDATE THEMicrosoft.Extensions.Logging
dependencies to latest versions.This step of upgrading is not absolutely required, but it's a good practice to start a project with the latest version of the library.
Right-click on the
.Wasm
project in the Solution Explorer and pickSet as Startup Project
.Note: this article will concentrate on build Wasm-only code, so it won't compile on other platforms' projects.
Press
CTRL-F5
. App should compile and start a browser session showing this:Note: when compiling using Uno platform the first time, it could take some time to download the latest .NET for WebAssembly SDK into a temporary folder.
2. Create a control in managed code
🎯 In this section, a control named PrismJsView
is created in code and used in the XAML page (MainPage.xaml
) to present it.
From the
[MyApp]
project, create a new class file namedPrismJsView.cs
. and copy the following code:using System; using System.Collections.Generic; using System.Text; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Markup; using Uno.Foundation; using Uno.UI.Runtime.WebAssembly; namespace PrismJsDemo.Shared { [ContentProperty(Name = "Code")] [HtmlElement("code")] // PrismJS requires a <code> element public class PrismJsView : Control { // ************************* // * Dependency Properties * // ************************* public static readonly DependencyProperty CodeProperty = DependencyProperty.Register( "Code", typeof(string), typeof(PrismJsView), new PropertyMetadata(default(string), CodeChanged)); public string Code { get => (string)GetValue(CodeProperty); set => SetValue(CodeProperty, value); } public static readonly DependencyProperty LanguageProperty = DependencyProperty.Register( "Language", typeof(string), typeof(PrismJsView), new PropertyMetadata(default(string), LanguageChanged)); public string Language { get => (string)GetValue(LanguageProperty); set => SetValue(LanguageProperty, value); } // *************** // * Constructor * // *************** public PrismJsView() { // Any HTML initialization here } // ****************************** // * Property Changed Callbacks * // ****************************** private static void CodeChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args) { // TODO: generate HTML using PrismJS here } private static void LanguageChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args) { // TODO: generate HTML using PrismJS here } } }
This will define a control having 2 properties, one code
Code
and another one forLanguage
.Change the
MainPage.xaml
file to the following content:<Page x:Class="PrismJsDemo.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PrismJsDemo" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Padding="10"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBox x:Name="lang" Text="csharp" Grid.Row="0" /> <TextBox x:Name="code" Text="var x = 3;
var y = 4;" AcceptsReturn="True" VerticalAlignment="Stretch" Grid.Row="1" /> <Border BorderBrush="Blue" BorderThickness="2" Background="LightBlue" Padding="10" Grid.Row="2"> <local:PrismJsView Code="{Binding Text, ElementName=code}" Language="{Binding Text, ElementName=lang}"/> </Border> </Grid> </Page>
Press CTRL-F5. You should see this:
Press F12 (on Chrome, may vary on other browsers).
Click on the first button and select the light-blue part in the app.
It will bring the DOM explorer to a
xamltype=Windows.UI.Xaml.Controls.Border
node. ThePrismJsView
should be right below after opening it.The
xamltype="PrismJsDemo.Shared.PrismJsView"
control is there!
👌 The project is now ready to integrate PrismJS.
3. Add JavaScript & CSS files
🎯 In this section, PrismJS files are downloaded from their website and placed as assets in the app.
Go to Prism download page.
Choose desired Themes & Languages (
Default
theme + all languages is used for the demo).Press the
DOWNLOAD JS
button and put theprism.js
file in theWasmScripts
folder of the.Wasm
project.Putting the
.js
file in this folder will instruct the Uno Wasm Bootstrapper to automatically load the JavaScript file during startup.Press the
DOWNLOAD CSS
button and put theprism.css
file in theWasmCSS
folder of the.Wasm
project.Putting the
.css
file in this folder will instruct the Uno Wasm Bootstrapper to automatically inject a<link>
HTML instruction in the resultingindex.html
file to load it with the browser.Right-click on the
.Wasm
project node in the Solution Explorer, and pickEdit Project File
(it can also work by just selecting the project, if thePreview Selected Item
option is activated).Insert this in the appropriate
<ItemGroup>
:<ItemGroup> <EmbeddedResource Include="WasmCSS\Fonts.css" /> <EmbeddedResource Include="WasmCSS\prism.css" /> <!-- This is new --> <EmbeddedResource Include="WasmScripts\AppManifest.js" /> <EmbeddedResource Include="WasmScripts\prism.js" /> <!-- This one too --> </ItemGroup>
For the Uno Wasm Bootstrapper to take those files automatically and load them with the application, they have to be put as embedded resources. A future version of Uno may remove this requirement.
Compile & run
Once loaded, press F12 and go into the
Sources
tab. Bothprism.js
&prism.css
files should be loaded this time.
4. Invoke JavaScript from Managed Code
🎯 In this section, PrismJS is used from the app.
First, there is a requirement for PrismJS to set the
white-space
style at a specific value, as documented here. An easy way to do this is to set in directly in the constructor like this:public PrismJsView() { // This is required to set to <code> style for PrismJS to works well // https://github.com/PrismJS/prism/issues/1237#issuecomment-369846817 this.SetCssStyle("white-space", "pre-wrap"); }
Now, we need to create an
UpdateDisplay()
method, used to generate HTML each time there's a new version to update. Here's the code for the method to add in thePrismJsView
class:private void UpdateDisplay(string oldLanguage = null, string newLanguage = null) { string javascript = $@" (function(){{ // Prepare Prism parameters const code = ""{WebAssemblyRuntime.EscapeJs(Code)}""; const oldLanguageCss = ""language-{WebAssemblyRuntime.EscapeJs(oldLanguage)}""; const newLanguageCss = ""language-{WebAssemblyRuntime.EscapeJs(newLanguage)}""; const language = ""{WebAssemblyRuntime.EscapeJs(newLanguage ?? Language)}""; // Process code to get highlighted HTML const prism = window.Prism; let html = code; if(prism.languages[language]) {{ // When the specified language is supported by PrismJS... html = prism.highlight(code, prism.languages[language], language); }} // Display result element.innerHTML = html; // Set CSS classes, when required if(oldLanguageCss) {{ element.classList.remove(oldLanguageCss); }} if(newLanguageCss) {{ element.classList.add(newLanguageCss); }} }})();"; this.ExecuteJavascript(javascript); }
Change
CodeChanged()
andLanguageChanged()
to call the newUpdateDisplay()
method:private static void CodeChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args) { (dependencyobject as PrismJsView)?.UpdateDisplay(); } private static void LanguageChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args) { (dependencyobject as PrismJsView)?.UpdateDisplay(args.OldValue as string, args.NewValue as string); }
We also need to update the result when the control is loaded in the DOM. So we need to change the constructor again like this:
public PrismJsView() { // This is required to set to <code> style for PrismJS to works well // https://github.com/PrismJS/prism/issues/1237#issuecomment-369846817 this.SetCssStyle("white-space", "pre-wrap"); // Update the display when the element is loaded in the DOM Loaded += (snd, evt) => UpdateDisplay(newLanguage: Language); }
Compile & run. It should work like this:
🔬 Going further
This sample is a very simple integration as there is no callback from HTML to managed code and PrismJS is a self-contained framework (it does not download any other JavaScript dependencies). Some additional improvements can be done to make the code more production ready:
- Make the control multi-platform. A simple way would be to use a WebView on other platforms, giving the exact same text-rendering framework everywhere. The code of this sample won't compile on other targets.
- Create script files instead of generating dynamic JavaScript. That would have the advantage of improving performance and make it easier to debug the code. A few projects are also using TypeScript to generate JavaScript. This approach is done by Uno itself for the
Uno.UI.Wasm
project. - Support more PrismJS features. There are many plugins for PrismJS that can be used. Most of them are very easy to implement.
- Continue with Part 3 - an integration of a more complex library with callbacks to application.