Tagged template literals are a ES2015 feature which,afaict, cannot be emitted directly. Template literals have an interesting property where a function can be used as a tag. EG:
~
function myTemplate(strings, ...args) {
return { strings: strings, args: args };
}
var someVar = "test"
var result = myTemplate string 0 ${someVar} string 1
~
Which, result in result having the value { strings: [ "string 0 ", " string 1"], args: ["test"]}
The application of the template literal to the function requires the abstract operation GetTemplateObject to be invoked which performs the splitting of the template into strings and args. ( https://www.ecma-international.org/ecma-262/6.0/#sec-gettemplateobject ) This abstract operation, as best I can tell, does not have an equivalent function at runtime. The language implementation is required to perform the operation when a function is applied to a template literal.
Libraries like lit-element rely on this feature. I've tried a few workarounds but none succeeded in implementing the required behavior exactly.
The closest workaround I have is using js.Function to evaluate a function dynamically defined:
~
val capture = js.Function("element", s"""
function captureTemplate(strings, ...args) {
return { strings: strings, args: args };
}
function inner() {
return captureTemplate${raw};
}
return inner.bind(element)();
""").asInstanceOf[js.Function1[js.Object, TemplateCapture]]
~
Where raw is the template literal text and TemplateCapture is
~
@js.native
trait TemplateCapture extends js.Object {
val strings: js.Array[String]
val args: js.Array[js.Any]
}
~
This is close enough for my purposes. Only a bit awkward.
Template strings in JavaScript are just syntactic sugar. The spec has this very elaborated definition of template literals based on this GetTemplateObject, but that's only because the ES spec has no notion of syntactic sugar. We can equivalently specify template strings in terms of their desugaring:
f`str0${expr0}str1${expr1}...${exprN-1}strN`
is desugared into
f(["str0", "str1", ..., "strN"], expr0, expr1, ..., exprN-1)
Therefore, a call such as the one in lit-element's first example:
html`<p>Hello, ${this.name}!</p>`
is equivalent to
html(["<p>Hello, ", "!</p>"], this.name)
and can therefore be written in Scala.js as:
html(js.Array("<p>Hello, ", "!</p>"), this.name)
provided that html was define in a native JS class/trait/object as
def html(strings: js.Array[String], exprs: Any*): TemplateResult = js.native
(where TemplateResult is lit-html's result type for its html template strings)
Now that might not be convenient, but it is 100% semantically accurate. You can make it convenient by defining, in Scala.js, an equivalent string interpolator that then calls the raw html, as follow:
import scala.scalajs.js.JSConverters._
object Interpolators {
implicit class LitHTMLInterpolator(private val sc: StringContext) extends AnyVal {
def html(exprs: Any*): LitHTML.TemplateResult =
LitHTML.html(sc.parts.toJSArray.map(StringContext.processEscapes), exprs: _*)
}
}
which you can then use as:
import Interpolators._
html"""<p>Hello, ${this.name}!</p>"""
making it completely equivalent to the original JS template string.
Conclusion: Scala.js already contains all the tools that you need to perfectly define string template-based APIs.
For reference and FTR, herre is a full executable example:
package helloworld
import scala.scalajs.js
import scala.scalajs.js.annotation._
import scala.scalajs.js.JSConverters._
// Fake implementation of lit-html's `html` method
object LitHTMLImpl {
@JSExportTopLevel("html")
def html(strings: js.Array[String], exprs: Any*): LitHTML.TemplateResult =
(strings.toList, exprs.toList)
}
@JSGlobalScope
@js.native
object LitHTML extends js.Any {
type TemplateResult = (List[String], List[Any])
def html(strings: js.Array[String], exprs: Any*): TemplateResult = js.native
}
object Interps {
implicit class LitHTMLInterpolator(private val sc: StringContext) extends AnyVal {
def html(exprs: Any*): LitHTML.TemplateResult =
LitHTML.html(sc.parts.toJSArray.map(StringContext.processEscapes), exprs: _*)
}
}
import Interps._
object HelloWorld {
def main(args: Array[String]): Unit = {
// Self-test that the fake implem corresponds to what a template string does
js.eval("""console.log("" + html`<p>Hello,\n ${42}!</p>`);""")
val foo = 42
val s = html"""<p>Hello,\n $foo!</p>"""
println(s)
}
}
One can even abstract away a utility method to make it easier to write such interpolators, including full processing of escapes and the raw field for non-processed strings:
package helloworld
import scala.scalajs.js
import scala.scalajs.js.annotation._
import scala.scalajs.js.JSConverters._
/** The type of the first argument to JS template string functions.
* It is an array of string with an addition `raw` property for the
* raw strings, not processed for escapes.
*/
trait JSTemplateStringsArg extends js.Array[String] {
def raw: js.Array[String]
}
/** Generic utility to write a Scala.js string interpolator that
* maps to a JS string template function.
*
* See `MyInterpolators` for usage.
*/
object JSTemplateStringInterpolator {
def apply[A, B](templateFun: (JSTemplateStringsArg, Seq[A]) => B, sc: StringContext, exprs: Seq[A]): B =
templateFun(JSTemplateStringInterpolator.strTemplateArgFor(sc), exprs)
private def strTemplateArgFor(sc: StringContext): JSTemplateStringsArg = {
val rawParts = sc.parts.toJSArray
val arg = rawParts.map(StringContext.processEscapes)
arg.asInstanceOf[js.Dynamic].raw = rawParts
arg.asInstanceOf[JSTemplateStringsArg]
}
}
// Fake implementation of lit-html's `html` method
object LitHTMLImpl {
@JSExportTopLevel("html")
def html(strings: js.Array[String], exprs: Any*): LitHTML.TemplateResult =
(strings.toList, exprs.toList)
}
/** Facade for the `html` interpolator, in the style of lit-html. */
@JSGlobalScope
@js.native
object LitHTML extends js.Any {
type TemplateResult = (List[String], List[Any])
def html(strings: js.Array[String], exprs: Any*): TemplateResult = js.native
}
/** Facade for the standard `String.raw` interpolator of JS. */
@JSGlobal("String")
@js.native
object JSString extends js.Any {
def raw(strings: JSTemplateStringsArg, exprs: Any*): String = js.native
}
object MyInterpolators {
implicit class LitHTMLInterpolator(private val sc: StringContext) extends AnyVal {
def html(exprs: Any*): LitHTML.TemplateResult =
JSTemplateStringInterpolator(LitHTML.html, sc, exprs)
}
implicit class JSRawInterpolator(private val sc: StringContext) extends AnyVal {
def jsraw(exprs: Any*): String =
JSTemplateStringInterpolator(JSString.raw, sc, exprs)
}
}
object HelloWorld {
def main(args: Array[String]): Unit = {
import MyInterpolators._
// Self-test that the fake implem corresponds to what a template string does
js.eval("""console.log("" + html`<p>Hello,\n ${42}!</p>`);""")
@noinline def foo: Int = 42
val s = html"""<p>Hello,\n $foo!</p>"""
println(s)
println(jsraw"""<p>Hello,\n $foo!</p>""")
}
}
Thanks for the detailed response! This worked great!
That last solution you propose really is nice and generic. How about a feature request to add that to the js library? If that sounds appropriate I'll open the ticket. :-)
I'm afraid we don't add non-essential stuff in the core repo anymore. We encourage people to publish small libraries with that kind of sugar, we're happy to advertise them on the website, and if they gain traction we can consider them for the stdlib (assuming they relate to the Scala spec and/or the ES spec, which is the case here) once they are completed vetted and super-stable.
An advantage of a separate library in this case would be that it could use macros to rewrite away all the overhead. In the core repo we do not use any macros anymore, since they are not compatible with Scala 3 in the general case.
We want scala-js/scala-js to be 100% stable, backwards binary compatible forever, so we cannot afford introducing anything non-essential.
A good plan IMO. I'll see about publishing a library with an equivalent.