# Render Functions
This page comes from the official Vue.js documentation and has been adapted for Vue GWT.
# Basics
Vue recommends using templates to build your HTML in the vast majority of cases. There are situations however, where you really need the full programmatic power of Java. That's where you can use the render function, a closer-to-the-compiler alternative to templates.
WARNING
Render function haven't been tested a lot with Vue GWT, so beware of bugs.
Let's dive into a simple example where a render
function would be practical. Say you want to generate anchored headings:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
For the HTML above, you decide you want this component interface:
<anchored-heading :level="1">Hello world!</anchored-heading>
When you get started with a component that just generates a heading based on the level
prop, you quickly arrive at this:
<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</script>
@Component
public class AnchoredHeadingComponent implements IsVueComponent {
@Prop(required = true)
Integer level;
}
That template doesn't feel great.
It's not only verbose, but we're duplicating <slot></slot>
for every heading level and will have to do the same when we add the anchor element.
While templates work great for most components, it's clear that this isn't one of them. So let's try rewriting it with a render
function:
@Component(hasTemplate = false)
public class AnchoredHeadingComponent implements IsVueComponent, HasRender {
@Prop(required = true)
Integer level;
@Override
public VNode render(VNodeBuilder builder) {
return builder.el("h" + this.level, vue().$slots().get("default"));
}
}
As you may notice, several things are happening:
- We pass the
hasTemplate
property to false to the@Component
annotation. This tells Vue GWT that this component doesn't have a template. - We implements the
HasRender
java interface and Override therender
function defined in it.
Still the resulting Component is much simpler! Sort of.
The code is shorter, but also requires greater familiarity with Vue instance properties.
In this case, you have to know that when you pass children without a slot
attribute into a component, like the Hello world!
inside of anchored-heading
, those children are stored on the component instance at $slots.default
.
If you haven't already, it's recommended to read through the instance properties API before diving into render functions.
# The VNodeBuilder
Instance
The second thing you'll have to become familiar with is how to use template features in the VNodeBuilder
builder.
This Vue GWT object wraps the Vue.js createElement
function to provide a cleaner Java interface.
Here is the VNodeBuilder
class:
public class VNodeBuilder {
...
/**
* Create an empty VNode
* @return a new empty VNode
*/
public VNode el() { ... }
/**
* Create a VNode with the given HTML tag
* @param tag HTML tag for the new VNode
* @param children Children
* @return a new VNode of this tag
*/
public VNode el(String tag, Object... children) { ... }
/**
* Create a VNode with the given HTML tag
* @param tag HTML tag for the new VNode
* @param data Information for the new VNode (attributes...)
* @param children Children
* @return a new VNode of this tag
*/
public VNode el(String tag, VNodeData data, Object... children) { ... }
/**
* Create a VNode with the given {@link IsVueComponent}
* @param isVueComponentClass Class for the {@link IsVueComponent} we want
* @param data Information for the new VNode (attributes...)
* @param children Children
* @return a new VNode of this Component
*/
public VNode el(Class<IsVueComponent> isVueComponentClass, VNodeData data, Object... children) { ... }
/**
* Create a VNode with the {@link IsVueComponent} of the given {@link VueComponentFactory}
* @param vueFactory {@link VueComponentFactory} for the Component we want
* @param children Children
* @return a new VNode of this Component
*/
public VNode el(VueComponentFactory<IsVueComponent> vueFactory, Object... children) { ... }
/**
* Create a VNode with the {@link IsVueComponent} of the given {@link VueJsConstructor}
* @param vueJsConstructor {@link VueJsConstructor} for the Component we want
* @param children Children
* @return a new VNode of this Component
*/
public VNode el(VueConstructor<Vue> vueConstructor, Object... children) { ... }
}
# The VNodeData
Object In-Depth
One thing to note: similar to how v-bind:class
and v-bind:style
have special treatment in templates, they have their own top-level fields in VNodeData
objects.
This object also allows you to bind normal HTML attributes as well as DOM properties such as innerHTML
(this would replace the v-html
directive).
Here is the JavaScript definition of the Data Object:
{
// Same API as `v-bind:class`
'class': {
foo: true,
bar: false
},
// Same API as `v-bind:style`
style: {
color: 'red',
fontSize: '14px'
},
// Normal HTML attributes
attrs: {
id: 'foo'
},
// Component props
props: {
myProp: 'bar'
},
// DOM properties
domProps: {
innerHTML: 'baz'
},
// Event handlers are nested under `on`, though
// modifiers such as in `v-on:keyup.enter` are not
// supported. You'll have to manually check the
// keyCode in the handler instead.
on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using `vm.$emit`.
nativeOn: {
click: this.nativeClickHandler
},
// Custom directives. Note that the binding's
// oldValue cannot be set, as Vue keeps track
// of it for you.
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// Scoped slots in the form of
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// The name of the slot, if this component is the
// child of another component
slot: 'name-of-slot',
// Other special top-level properties
key: 'myKey',
ref: 'myRef'
}
# Complete Example
With this knowledge, we can now finish the component we started:
@Component(hasTemplate = false)
public class AnchoredHeadingComponent implements IsVueComponent, HasRender {
private static RegExp camelCasePattern = RegExp.compile("([a-z])([A-Z]+)", "g");
@Prop(required = true)
Integer level;
@Override
public VNode render(VNodeBuilder builder) {
String text =
getChildrenTextContent(vue().$slots().get("default")).trim().replaceAll(" ", "-");
String headingId = camelCasePattern.replace(text, "$1-$2").toLowerCase();
return builder.el("h" + this.level,
builder.el("a",
VNodeData.get().attr("name", headingId).attr("href", "#" + headingId),
vue().$slots().get("default")));
}
private String getChildrenTextContent(JsArray<VNode> children) {
return children.map(child -> {
if (child.getChildren() != null && child.getChildren().length > 0)
return getChildrenTextContent(child.getChildren());
return child.getText();
}).join("");
}
}
Used in a Component with this template:
<div>
<anchored-heading level="4">This is H4</anchored-heading>
<anchored-heading level="5">This is a H5</anchored-heading>
</div>
Results in:
# Constraints
# VNodes Must Be Unique
All VNodes in the component tree must be unique. That means the following render function is invalid:
@Override
public VNode render(VNodeBuilder builder) {
// Don't do that!
VNode myParagraph = builder.el("p", "Hello World");
return builder.el("div", myParagraph, myParagraph);
}
# Replacing Template Features with Plain Java
# v-if and v-for
Wherever something can be easily accomplished in plain JavaScript, Vue render functions do not provide a proprietary alternative.
For example, in a template using v-if
and v-for
:
<ul v-if="items.length">
<li v-for="Item item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
This could be rewritten with Java if
/else
and map
in a render function:
@Override
public VNode render(VNodeBuilder builder) {
if (this.items.length) {
return builder.el("ul", this.items.map(item -> {
return builder.el("li", item.getName()));
}));
}
return createElement("p", "No items found.");
}
# v-model
There is no direct v-model
counterpart in render functions - you will have to implement the logic yourself.
This is the cost of going lower-level, but it also gives you much more control over the interaction details compared to v-model
.
# Event & Key Modifiers
For the .passive
, .capture
and .once
event modifiers, Vue offers prefixes that can be used with on
:
Modifier(s) | Prefix |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.capture.once or .once.capture | ~! |
For example:
vNodeData.on("!click", () -> {this.doThisInCapturingMode();})
.on("~keyup", () -> {this.doThisOnce();})
.on("~!mouseover", () -> {this.doThisOnceInCapturingMode();});
}
For all other event and key modifiers, no proprietary prefix is necessary, because you can simply use event methods in the handler.
Modifier(s) | Equivalent in Handler |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
Keys:.enter , .13 | if (event.keyCode !== 13) return (change 13 to another key code for other key modifiers) |
Modifiers Keys:.ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (change ctrlKey to altKey , shiftKey , or metaKey , respectively) |
Here's an example with all of these modifiers used together:
vNodeData.on("keyup", (param) -> {
KeyboardEvent event = (KeyboardEvent) param;
// Abort if the element emitting the event is not
// the element the event is bound to
if (event.target != event.currentTarget) return;
// Abort if the key that went up is not the enter
// and the shift key was not held down
// at the same time
if (!event.shiftKey || !"Enter".equals(event.code)) return;
// Stop event propagation
event.stopPropagation();
// Prevent the default keyup handler for this element
event.preventDefault();
// ...
}
}
# Slots
You can access static slot contents as Arrays of VNodes from vue().$slots()
:
@Override
public VNode render(VNodeBuilder builder) {
// `<div><slot></slot></div>`
return builder.el("div", vue().$slots().get("default"));
}
And access scoped slots as functions that return VNodes from vue().$scopedSlots()
:
@Override
public VNode render(VNodeBuilder builder) {
// `<div><slot></slot></div>`
return builder.el(
"div",
vue().$scopedSlots().get("default").execute(JsObject.of("text", this.msg))
);
}
To pass scoped slots to a child component using render functions, use the scopedSlots
field in VNode data:
vNodeData.scopedSlot("default", props -> {
JsObject jsProps = (JsObject) props;
return builder.el("span", jsProps.get("text"));
});
# JSX
Vue.js support JSX templates this is not supported by Vue GWT.
# Functional Components
Vue.js support Functional Components this is not yet supported by Vue GWT.
# Template Compilation
Vue GWT uses Vue.js template compiler at Java compile time to compile your HTML template in JavaScript render functions. If you are curious about how Vue.js compile templates you can check the template compiler on their documentation.