Calling JavaScript
Transpose gives you two levels of access to JavaScript: raw injection for one-offs,
and typed [External] bindings for anything you use more than once. Prefer the
bindings — they are checked at compile time and cost nothing at runtime.
Script.Write — inject raw JavaScript
Script.Write from the Transpose namespace emits its argument into the generated
code verbatim.
using Transpose;
public void SayHello()
{
// emits: alert('Hello from JavaScript!');
Script.Write("alert('Hello from JavaScript!');");
}
Use {0}, {1} … to splice in the emitted form of the following arguments:
public void LogMessage(string message)
{
// emits: console.log(message);
Script.Write("console.log({0});", message);
}
Script.Write<T> is the expression form — it returns a value, so it can be used
inside an expression:
double now = Script.Write<double>("performance.now()");
Three things to know:
- The code string must be a compile-time constant. A
stringvariable, an interpolated string, or a concatenation is not substituted; the call is emitted as an ordinary method call instead, which will not resolve at runtime. - With no substitution arguments the string is injected exactly as written, so
{and}are literal. This is what makes a regular-expression literal like"/^(.{8})(.{4})$/"work. As soon as you pass an argument,{n}becomes a placeholder. - The injected code is not validated. A syntax error inside it is a syntax error in the whole bundle, so nothing on the page runs. If a build produces a blank page, this is the first thing to check.
Bare identifiers inside the string refer to whatever is in scope in the emitted
JavaScript. Because Transpose keeps C# local names where JavaScript allows it, a C#
local usually is reachable by its own name — but that is a coincidence of naming,
not a substitution. Pass values through {0} instead, so a rename cannot silently
break the template.
Script.Eval
Script.Eval emits a call to the JavaScript eval, evaluating a string at
runtime:
public int Add(int a, int b)
{
return Script.Eval<int>($"{a} + {b}");
}
Avoid `Script.Eval`
Unlike Script.Write, this really is runtime eval: it is slow, it defeats the
engine's optimiser, and it is blocked outright by a Content-Security-Policy that
does not allow 'unsafe-eval'. Everything Script.Eval can do, Script.Write
does at compile time. Reach for it only when the code genuinely is not known until
runtime.
Dynamic access helpers
Script also exposes helpers for reaching JavaScript by name, which is useful for
globals you do not want to write a binding for:
using Transpose;
// Call a global function
Script.Call("myFunction", "arg1", 123);
var result = Script.Call<int>("computeTotal");
// Read and write by name
var title = Script.Get<string>("document.title");
Script.Set("window.myVar", "value");
// Probe a value
bool present = Script.IsDefined(someObject);
bool isArray = Script.IsArray(someObject);
string kind = Script.TypeOf(someObject);
These are string-keyed, so a typo is a runtime failure. A typed binding is better wherever the API is stable.
[External] — typed bindings
[External] declares that a type or member already exists in JavaScript. Transpose
emits no implementation for it and binds calls straight through to the JavaScript
name.
using Transpose;
[External]
[Name("Chart")]
public class Chart
{
public extern Chart(object canvas, object config);
public extern void update();
public extern void destroy();
}
// Usage — emits: new Chart(canvas, config)
var chart = new Chart(canvas, config);
chart.update();
[External] can also be applied to a whole assembly with
[assembly: External], which is how the binding libraries (Transpose.Core,
Transpose.HttpClient, …) are built: every type in them is a binding.
Member names on an external type are camelCased by default, matching JavaScript
convention. Use [Name] to pin an exact name.
[Name] — pin the emitted name
[External]
[Name("Math")]
public static class JsMath
{
[Name("random")]
public static extern double Random();
[Name("max")]
public static extern double Max(double a, double b);
}
double r = JsMath.Random(); // emits: Math.random()
[Template] — control the emitted call
[Template] replaces a member's call site with a JavaScript expression of your
own. This is how you adapt a JavaScript API to a signature that reads well in C#.
[External]
public static class Log
{
[Template("console.log({0})")]
public static extern void Message(object message);
[Template("console.warn({0}, {1})")]
public static extern void Warn(string category, string message);
}
Log.Message("Hello") emits console.log("Hello").
Placeholders:
{0},{1}, … — the arguments, in order.{paramName}— an argument by parameter name, which survives reordering.{this}— the receiver, for an instance member.
[External]
public class Element
{
[Template("{this}.setAttribute({name}, {value})")]
public extern void SetAttr(string name, string value);
}
A templated member is inlined at every call site, is excluded from overload numbering, and does not thread type arguments — so a template is also the way to bind a generic helper without the runtime type-argument machinery.
[Script] — a hand-written body
[Script] supplies the JavaScript body of a method or accessor. The C# body,
if there is one, is discarded — which means an extern member can be given an
implementation in JavaScript:
public class Clipboard
{
[Script("navigator.clipboard.writeText(text);")]
public extern void Copy(string text);
}
Parameters are in scope by their emitted names. Pass several strings for several lines.
Use [Script] when the implementation is a few statements; use [Template] when
it is a single expression that should be inlined.
[GlobalMethods] and [Scope]
[GlobalMethods] on a static class projects its members onto the JavaScript global
scope, so the type name disappears from the call:
[External]
[GlobalMethods]
public static class Globals
{
public static extern void alert(string message);
public static extern double parseFloat(string value);
}
Globals.alert("hi"); // emits: alert("hi")
[Scope("prefix")] does the same against a named binding rather than the bare
global — it is what puts the DOM types under Transpose.Core.dom while emitting
window.foo rather than dom.window.foo.
[ExpandParams] — variadic JavaScript functions
A params array is passed as one array by default. [ExpandParams] spreads it as
individual arguments, which is what a variadic JavaScript function expects:
[External]
public static class Console
{
[Name("log")]
[ExpandParams]
public static extern void Log(params object[] values);
}
Console.Log("a", 1, true); // emits: console.log("a", 1, true)
Loading the JavaScript library itself
A binding says how to call a library; it does not load it. Add the library to the page in one of two ways:
- List it in the
resourcessection oftps.json, which copies it into the output and adds a<script>tag to the generatedindex.html. See Resources. - Add it to
html.headintps.jsonas a<script src="…">pointing at a CDN.
Because a Transpose bundle is not a JavaScript module, an npm package has to expose a global for a binding to reach it. See Output Types.
Attributes that are not implemented
If you are porting bindings from h5, note that [Module] / [ModuleDependency]
(emit under a module system), [Mixin], [Constructor], [Field] and [Cast]
are not read by Transpose. See the
Attribute Reference for the full status table.