Update 2023-05-09: Noticed this super old post still gets a bunch of visits (gratifying but strange). But the technique in this post hasn’t worked in Marketo emails for a long time, since Marketo changed their Velocity setup. Sorry, you have to use pure Velocity now! (Of course, Velocity in other contexts may still support it.)
Since version 1.6 (i.e. a long time ago) Java[1] has had a built-in JavaScript engine. Called Rhino in Java 1.6-1.7 and replaced by Nashorn in 1.8, the JS engine has always been reliable (it’s the user-facing script language of numerous Java-based commercial apps) even if its function set and performance lags behind browsers’ JS engines.
This means if you have a ready-made JS snippet that does what you want, and you don't want to learn Velocity and/or translate your JS into native Java methods (hint: you eventually should learn how to tie Velocity in with raw Java, you'll be more productive for it) then you can sneak JS inside your Velocity token and get your cool email personalizations out the door.
You'll be running JavaScript, inside Velocity, inside Java, all inside Marketo: it's pretty darn cool, even if not something to lean on permanently.
Here's an example Velocity script which has JS do the heavy lifting:
## Just locating and loading the engine here, but it's gotta be verbose!
#set( $javaVersion = $context.getClass().forName(
"java.lang.System"
).getProperty( "java.specification.version" ) )
#if( $javaVersion.equals("1.6") || $javaVersion.equals("1.7") )
#set( $JSEngine = $context.getClass().forName(
"com.sun.script.javascript.RhinoScriptEngine"
).newInstance() )
#else
#set( $JSEngine = $context.getClass().forName(
"javax.script.ScriptEngineManager"
).newInstance().getEngineByName("JavaScript") )
#end
## Now to actually use the engine...
## Define a string of pure JavaScript
#define( $myJS )
/* Sure, you can use JS comments */
var altCase = [];
letters = String(lead.get('FirstName')).split('');
for ( var i=0, imax=letters.length; i<imax; i++ ) {
var letter = letters[i];
altCase.push( i % 2 ? letter.toLowerCase() : letter.toUpperCase() );
}
var spacedOutInJS = altCase.join(' ');
#end
## Export Velocity's $lead as JS lead
$JSEngine.put("lead", $lead)
## Execute the JS and pluck out a result variable
#set( $tmp = $JSEngine.eval($myJS.toString()) )
$JSEngine.get("spacedOutInJS")
If the lead's First Name is
Yongsong
this purposely playful script will output
Y o N g S o N g
As you can see, after some typically verbose Velocity to load the JS engine, I simply
- store JS code in a text string (I used
#define
for the multiline string but you can use#set
just as well) - pass the code to
$JSEngine.eval()
- grab variables out with
$JSEngine.get()
.
Aside from the engine setup, the string manipulation in Velocity alone would be far more verbose and cluttered with #
s. Not every task is easier to build in JS, of course (the more you learn about native Java objects + methods within Velocity, the less tempted you'll be by embedded JS). But for a Velocity newbie who knows a little JS, this can be a real leg up.
What JS functions are supported?
Marketo's current environment still uses Java 1.6, so its Rhino JS engine is as old as it gets. According to Sun, it supports all of JavaScript 1.5. For reference, this is the version supported by Internet Explorer 6.
(When Marketo moves to the Google Cloud, I assume they'll bump their Java version up to 1.8, at which point the accompanying Nashorn JavaScript engine will be far more modern — somewhere between ECMAScript 5 and ECMAScript 2015. The script above will auto-adjust to find the best engine.)
But don't let the JS version alone scare you off. Missing ES5 magic like map
and forEach
is painful (that's why you see me using an old for
loop above, which I haven't written in maybe 2 years!) but the main gains are:
- familiarity, if you're already a little bit into JS (almost no one is a little bit into Velocity, you're either deep into it or a newbie)
- smoother multiline code (without having Velocity
#
directives everywhere and needing to worry about whitespace) - access to certain operators which throw parsing errors in VTL (like the ternary
?:
) - ability to create reusable functions (Velocity
#macros
don't really fill the bill)
What should I do with this?
Up to you! Once you're reasonably fluent in Velocity and know how frustrating some patterns can be (most anything can be done in VTL + Java, but man, that code can be long and almost unreadable) and know your way around JavaScript, you'll know at a glance whether embedded JS would help. Until then, you'll have to write code and ask (yourself) questions later.
Here's one example where you might think JS is shorter, but it turns out to be merely more familiar.
Say Pets is is a multi-valued lead field like
dog;cat;wolf
and you want to display an alphabetized list
cat
dog
wolf
In JS-in-Velocity, it'd be like so:
#set( $myJS = "var sortedPets = String(lead.get('Pets')).split(';').sort().join('<br>');" )
#set( $tmp = $JSEngine.eval($myJS) )
${JSEngine.get("sortedPets")}
But in Velocity alone, that takes just one line:
${display.list( $sorter.sort($lead.Pets.split(";")), "<br>" )}
So I think Velocity wins here (though a few more sets of parentheses might change that). $display.list
is a really awesome Velocity shortcut that one wishes existed in pure Java. Get to know it!
On the flipside, JS definitely wins this one:
$JSEngine.eval( "encodeURIComponent(String(lead.get('Website')))" )
It's the clear winner because there's no competition: you can't do URL encoding in Marketo's Velocity + Java setup[2] without jumping through some major hoops (which I'll show in another blog post). That's sucky, because if you ever want to embed a lead field like Website or even Email in the query string of another URL, you must encode if you don't want the remote server to barf. Luckily, as a web-centric language, JS has included encodeURIComponent
since way back.
Why are you wrapping String values in String()
, though?
Good eye and good question. To do useful work with a String property on $lead
(injected into JavaScript as lead
without the dollar sign), you have to wrap it in String()
.
Remember, the $lead
object in Velocity is a Java HashMap, as I've discussed before. Velocity doesn't have simple datatypes of its own; it uses Java's own HashMap, ArrayList, String, Integer, Boolean, Date, Calendar, etc.
In pure Velocity, the props on the $lead
HashMap are all Strings (even if in the Marketo database they're Datetimes, Booleans, etc.). But those are Java Strings — instances of java.lang.String
— not JavaScript strings.
When you inject the HashMap into the JS engine, it becomes a strange and foreign beast (from JS's perspective) called a NativeJavaObject, which is not a JS Object but rather a wrapper for a Java Object.
The String properties on the original HashMap (now wrapped inside a NativeJavaObject) aren't directly accessible: you use built-in get
and put
accessors to read/write them. And when you get one, like when I call lead.get('Website')
above, it's going to be one of these crazy NativeJavaObjects as well! To make it into a JavaScript String so you can use JS methods like encodeURIComponent
on it (since why else would you be trying to do this crazy stuff anyway) you wrap it in String()
.
An Inception reference seems inevitable: here you have Java inside JavaScript inside Velocity inside Java!
Notes
[1] Sun- or Oracle-branded JVMs (runtime engines). Other Java implementations may or may not contain scripting engines. Also of note, maybe: Rhino was written by Mozilla, so it technically was 3rd-party software within the Oracle JVM. Nashorn is sponsored directly by Oracle (though open source).
[2] Because for some reason they didn't include the Velocity LinkTool, which is designed for this task.