doc/articles/interop/wasm-javascript-2.md
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.
š To reproduce the code in this article, you must prepare a development environment using Uno's Getting Started article.
šÆ This section is very similar to the Create a Counter App with Uno Platform tutorial in the official documentation.
Create a new Uno Platform App project using one of the getting started blank or recommended presets.
šÆ 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 named PrismJsView.cs. and copy the following code:
using System;
using System.Collections.Generic;
using System.Text;
using Uno.Foundation;
using Uno.UI.Runtime.WebAssembly;
namespace PrismJsDemo;
using Uno.UI.NativeElementHosting;
namespace PrismJsDemo;
class PrismJsView : ContentControl
{
private BrowserHtmlElement? _element;
// *************************
// * Dependency Properties *
// *************************
public static readonly DependencyProperty CodeProperty = DependencyProperty.Register(
nameof(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 LangProperty = DependencyProperty.Register(
"Lang",
typeof(string),
typeof(PrismJsView),
new PropertyMetadata(default(string), LangChanged));
public string Lang
{
get => (string)GetValue(LangProperty);
set => SetValue(LangProperty, value);
}
// ***************
// * Constructor *
// ***************
public PrismJsView()
{
_element = BrowserHtmlElement.CreateHtmlElement("code");
Content = _element;
}
// ******************************
// * Property Changed Callbacks *
// ******************************
private static void CodeChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args)
{
// TODO: generate HTML using PrismJS here
}
private static void LangChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args)
{
// TODO: generate HTML using PrismJS here
}
}
This will define a control having 2 properties, one code Code and another one for Lang.
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}" Lang="{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 code node. The PrismJsView should be right below after opening it.
The code control is there!
š The project is now ready to integrate PrismJS.
šÆ 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 the prism.js file in the WasmScripts folder of the .Wasm project.
Putting the
.jsfile in this folder will instruct the Uno Wasm Bootstrapper to automatically load the JavaScript file during startup.
Press the DOWNLOAD CSS button and put the prism.css file in the WasmCSS folder of the .Wasm project.
Putting the
.cssfile in this folder will instruct the Uno Wasm Bootstrapper to automatically inject a<link>HTML instruction in the resultingindex.htmlfile to load it with the browser.
Right-click on the .Wasm project node in the Solution Explorer, and pick Edit Project File (it can also work by just selecting the project, if the Preview 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. Both prism.js & prism.css files should be loaded this time.
šÆ 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()
{
_element = BrowserHtmlElement.CreateHtmlElement("code");
Content = _element;
// This is required to set to <code> style for PrismJS to works well
// https://github.com/PrismJS/prism/issues/1237#issuecomment-369846817
_element.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 the PrismJsView class:
private void UpdateDisplay(string oldLanguage = null, string newLanguage = null)
{
string javascript =
$$"""
(function(){
// Prepare Prism parameters
const code = "{{BrowserHtmlElement.EscapeJs(Code)}}";
const oldLanguageCss = "language-{{BrowserHtmlElement.EscapeJs(oldLanguage)}}";
const newLanguageCss = "language-{{BrowserHtmlElement.EscapeJs(newLanguage)}}";
const language = "{{BrowserHtmlElement.EscapeJs(newLanguage ?? Lang)}}";
// 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);
}
})();
""";
_element.ExecuteJavascript(javascript);
}
Change CodeChanged() and LangChanged() to call the new UpdateDisplay() method:
private static void CodeChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args)
{
(dependencyobject as PrismJsView)?.UpdateDisplay();
}
private static void LangChanged(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
_element.SetCssStyle("white-space", "pre-wrap");
// Update the display when the element is loaded in the DOM
Loaded += (snd, evt) => UpdateDisplay(newLanguage: Lang);
}
Compile & run. It should work like this:
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:
ExecuteJavascript. That would have the advantage of improving performance and making it easier to debug the code.