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, …), andDomObserver.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
objectand reach its methods throughScript.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.