Wrapping a JavaScript Library

Several Tesserae components are thin C# wrappers over JavaScript libraries — Masonry (masonry-layout), tooltips (tippy and popper), CodeDiff (diff2html), MarkdownBlock (marked and purify), SortableStack (sortable), and Diagram (baklava). The recipe is always the same: bundle the script, then drive its global from C# through Script.Write (see JavaScript Interop for the interop mechanics).

1. Bundle the library

Place the minified library under Tesserae/h5/assets/js/ and add it to the resource bundles in Tesserae/h5.json. It must appear in both the tss-dep.js and tss-dep.min.js bundles — h5 swaps between the minified and non-minified bundles for Release vs Debug builds, so the two file lists must stay in sync:

{
    "name": "tss-dep.js",
    "files": [
        "h5/assets/js/popper.min.js",
        "h5/assets/js/tippy.min.js",
        "h5/assets/js/masonry.min.js",
        "h5/assets/js/yourlib.min.js"
    ],
    "output": "assets/js"
}
// ...add the identical entry to the "tss-dep.min.js" bundle as well

Any CSS the library ships goes into the tss.css bundle the same way. Once it is bundled, the global the library exposes (for example Masonry, tippy, or Diff2HtmlUI) is available at runtime.

2. Wrap it in an IComponent

Build a host element, instantiate the library against it, and keep the JavaScript instance in an object field so you can call back into it. Masonry.cs is the canonical example:

using H5;
using static H5.Core.dom;
using static Tesserae.UI;

[H5.Name("tss.Masonry")]
public class Masonry : IContainer<Masonry, IComponent>, ISpecialCaseStyling
{
    private readonly HTMLElement _host;
    private readonly object      _instance;   // the live JS object
    private double               _timeout;

    public Masonry(int columns, int gutter = 10)
    {
        _host     = Div(_("tss-masonry"));
        _instance = Script.Write<object>(
            "new Masonry({0}, { itemSelector: '.tss-masonry-item', gutter: {1}, percentPosition: true })",
            _host, gutter);

        // libraries that measure the DOM must (re)run once the element is on-screen
        DomObserver.WhenMounted(_host, () => Layout());
    }

    public void Add(IComponent component)
    {
        var el = GetItem(component, true);
        _host.appendChild(el);
        Script.Write("{0}.appended({1})", _instance, el);   // call a JS method
        Layout();
    }

    private void Layout()
    {
        if (!_host.IsMounted()) return;
        window.clearTimeout(_timeout);                       // debounce relayout
        _timeout = window.setTimeout((_) => Script.Write("{0}.layout()", _instance), 16);
    }

    public HTMLElement Render() => _host;
}

What to watch for

  • Instantiate against a real element. Pass your host element as the first placeholder.
  • Defer DOM-measuring calls until the element is mounted. Use DomObserver.WhenMounted(element, …), and DomObserver.WhenRemoved(element, …) to tear the instance down. An element has no size until it is in the document.
  • Debounce expensive relayouts with window.setTimeout / window.clearTimeout.
  • Hold the instance as object and reach its methods through Script.Write("{0}.method({1})", instance, arg).
  • For a typed surface over the library instead of inline strings, declare an [External] / [H5.Name] binding — see JavaScript Interop.

3. Register the component

Finish as you would any component: add a factory in Tesserae/src/Base/UI.Components.cs and a sample under Tesserae.Tests/. See Creating a Component for the full checklist.

See also

© 2026 Curiosity. All rights reserved.