closure-builder
Version:
Simple Closure, Soy and JavaScript Build system
246 lines (189 loc) • 8.76 kB
Markdown
[TOC]
# Creating a Custom Plugin
`Closure Templates` allows users to write functions that templates can call. This
is useful for when there is some logic that is difficult or impossible to
express using `Closure Templates` language features. `Closure Templates` actually has
a number of these built in. For example, the `mapKeys` function which can be
used to get the keys of a map for iteration.
```soy
{template .foo}
{ m: map<string, string>}
{for $key in mapKeys($m)}
Key: {$key}, Value: {$m[$key]}
{/for}
{/template}
```
However, it isn't possible for `Closure Templates` to supply all possible desired
functionality so `Closure Templates` allows users to supply custom plugin function
definitions.
## When to define a custom function
Soy functions are a fairly powerful feature, but they aren't always the best
option. This is because:
* You need to write distinct implementations for all languages (JS, Java,
Python) that you are compiling your templates for and ensuring consistent
behavior can be difficult.
* The registration mechanism is cumbersome.
* It is difficult to produce `SanitizedContent` from a plugin, so they are a
poor fit for generating `html` `css` or `JavaScript`
So in general if you can represent your functionality using a shared template
you should do so. However, they are sometimes quite necessary. Some good
usecases include:
* Custom date or number formatting
* Complex math functions
* Date calculations
* Filtering, sorting or aggregating datastructures
* ...
All of the above are difficult or impossible to implement in a plain template.
For these reasons `Closure Templates` allows for users to write custom functions.
## How do I create a `SoySourceFunction`?
### 1. Define the signature `SoySourceFunction`
For example, let's assume you wanted to have a function called `uniqueId()` that
returns a unique number. This might be useful for generating DOM ids. You would
start by defining a `SoyFunction` subtype:
```java
(
name = "uniqueId",
value = (returnType="string"))
class UniqueIdFunction implements SoySourceFunction {}
```
This tells the compiler basic information for your function (name and arity),
however we don't yet have any implementations that would tell the compiler how
to generate code for this function.
### 2. Define the logic
Depending on what backends you care about, you can also have your function
implement any of the following interfaces:
1. `SoyJavaScriptSourceFunction` for generating JS code
1. `SoyJavaSourceFunction` for generating Java bytecode
1. `SoyPySrcFunction` for generating Python code
1. _experimental_ `LoggingFunction` for interacting with a `SoyLogger`. See the
[doc-logging](doc-logging#logging_function) guide for more information
For example, if you wanted to have an implementation for both Java and JS you
would implement both those interfaces and write something like
```java
(
name = "uniqueId",
value = (returnType="string"))
public class UniqueIdFunction implements
SoyJavaSourceFunction, SoyJavaScriptSourceFunction {
private static final AtomicLong counter = new AtomicLong();
public static String nextId() {
return "id-" + counter.incrementAndGet();
}
private static final Method NEXT_ID =
JavaValueFactory.createMethod(UniqueIdFunction.class, "nextId");
public JavaValue applyForJavaSource(
JavaValueFactory factory, List<JavaValue> args, JavaPluginContext context) {
return factory.callStaticMethod(NEXT_ID);
}
public JavaScriptValue applyForJavaScriptSource(
JavaScriptValueFactory factory, List<JavaScriptValue> args, JavaScriptPluginContext context) {
// Note: If the library isn't provided by goog.module, use callNamespaceFunction instead.
return factory.callModuleFunction("some.js.lib", "uniqueId");
}
}
```
Given this implementation, the `applyForJavaSource` method will invoked by the
compiler to generate Java bytecode to call the "nextId" method for server-side
rendering, and the `computeForJsSrc` method will be invoked by the compiler to
generate JS code for client-side rendering (the implementation will call the
given JS library function).
If the Java implementation needs any non-static dependencies at runtime (e.g, a
`NextIdService`), the `applyForJavaSource` method can use
`JavaValueFactory.callInstanceMethod`.For rendering to work, the instance the
plugin will use must be passed to the `SoySauce` or `SoyTofu` constructor, or if
that is impossible the Renderer's `setPluginInstances` method. For example,
```java
(
name = "uniqueId",
value = (returnType="string"))
public class UniqueIdFunction implements
SoyJavaSourceFunction, SoyJavaScriptSourceFunction {
private static final Method NEXT_ID =
JavaValueFactory.createMethod(NextIdService.class, "nextId");
public JavaValue applyForJavaSource(
JavaValueFactory factory, List<JavaValue> args, JavaPluginContext context) {
return factory.callInstanceMethod(NEXT_ID);
}
JavaScriptValue applyForJavaScriptSource(
JavaScriptValueFactory factory, List<JavaScriptValue> args, JavaScriptPluginContext context) {
// Note: If the library isn't provided by goog.module, use callNamespaceFunction instead.
return factory.callModuleFunction("some.js.lib", "uniqueId");
}
}
class NextIdService {
private final AtomicLong counter = new AtomicLong();
public String nextId() {
return "id-" + counter.incrementAndGet();
}
}
```
Once you are satisfied with the implementation, you would register it with the
compiler.
#### Contextual data available to plugins
For Java, JavaPluginContext provides some contextual data.
* getULocale
Provides the current locale being used for rendering, as a
`com.ibm.icu.util.ULocale`.
### 3. Register your plugin with the compiler
All SoySourceFunctions (e.g, `SoyJavaSourceFunction`) must be passed to the
compiler via the `--pluginFunctions` flag. The flag takes a comma separated list
of the fully qualified class name of the function. You will also need to make
sure that the class is on the compiler's classpath.
{{#internal}}
Google users should use the `af_soy_plugin` build rule to register their
plugins. See go/af-soy/build_rules#af-soy-plugin-and-af-soy-legacy-plugin
{{/internal}}
### 4. Use It!
At this point, you can start calling your new plugin function from your
templates. For example:
```soy
{template .foo}
{let $id : uniqueId() /}
<div id={$id}>
Some content
</div>
...
<a href="#{$id}">Scroll up</a>
{/template}
```
When rendered server side, the methods referenced in `applyForJavaSource` will
be invoked. If `applyForJavaSource` used `callInstanceMethod`, the instance
classes must be supplied to `SoySauce` or `Tofu` constructor (or
`Renderer.setPluginInstances` if the former is infeasible). When compiled for
JS, the compiler will output a `goog.require('some.js.lib');` and invoke
`some.js.lib.uniqueId()` for each call to `uniqueId()`.
## More about SoyFunctionSignature...
All types that are understandable by `Closure Templates` compiler will be supported
in the annotation.
Some plugins might behave differently when the input size changes. It is
possible to define some overloads in the function signature annotation:
```java
(
name = "foo",
value = {
(
parameterTypes = {"string"},
returnType = "int"
),
(
parameterTypes = {"string", "string"},
returnType = "string"
)})
class FooFunction implements SoySourceFunction {}
```
There are some restrictions to this annotation. First, it does not support
overloads that have the same amount of arguments. Second, generic overloads are
unsupported. It is impossible to say this function will return `T` when the
input is `list<T>`.
## Custom print directives, not recommended
`Closure Templates` also allows the creation of custom print directives, for
example, in `{$foo|truncate:100}` `truncate` is a print directive.
The method for authoring and configuring print directives is nearly identical to
configuring a custom function (described above). However, it is no longer
recommended for projects to create these. All the usecases for a custom print
directive can be satisfied by a custom function and the syntax/behavior of those
is more intuitive.