# Composing with Components
This page comes from the official Vue.js documentation and has been adapted for Vue GWT.
# Using Components
# What are Components?
Components are one of the most powerful features of Vue.
They help you extend basic HTML elements to encapsulate reusable code.
At a high level, components are custom elements that Vue's compiler attaches behavior to.
In some cases, they may also appear as a native HTML element extended with the special is
attribute.
# Registration
We already saw how to define a Component in Vue GWT.
@Component
public class MyChildComponent implements IsVueComponent {
}
<span>A custom child component!</span>
You can make a component available in the scope of another Component by passing it to the components
annotation option:
@Component(components = {MyChildComponent.class})
public class ParentComponent implements IsVueComponent {
}
<div>
<my-child></my-child>
</div>
Which will render:
<div>
<span>A custom child component!</span>
</div>
# How is the HTML Tag Name Determined?
The HTML tag name of your Component is determined using the Component's Class name.
The class name is converted from CamelCase to kebab-case. If the name ends with "Component" this part is dropped.
For example:
MyChildComponent -> my-child
TodoComponent -> todo
TodoListComponent -> todo-list
Header -> header
The same principle applies for other registrable Vue features, such as directives.
TIP
Note that Vue does not enforce the W3C Rules for custom tag names (all-lowercase, must contain a hyphen) though following this convention is considered good practice.
# Global Registration
To register a global component, you can use Vue.component(tagName, MyComponentFactory.get())
. For example:
Vue.component("my-component", MyComponentFactory.get());
Your Component will then be available in all the Components template from your application.
TIP
It's better to register locally whenever you can. Locally registered components get compile time type checking when binding property values. They also break at compile time if you forget a required property.
# DOM Template Parsing Caveats
There are some restrictions that are inherent to how HTML works, because Vue can only retrieve the template content after the browser has parsed and normalized it.
Most notably, some elements such as <ul>
, <ol>
, <table>
and <select>
have restrictions on what elements can appear inside them, and some elements such as <option>
can only appear inside certain other elements.
This will lead to issues when using custom components with elements that have such restrictions, for example:
<table>
<my-row>...</my-row>
</table>
The custom component <my-row>
will be hoisted out as invalid content, thus causing errors in the eventual rendered output.
A workaround is to use the is
special attribute:
<table>
<tr is="my-row"></tr>
</table>
# Data sharing between Components instances
If you know Vue.js then you know that the data
property of your Component must be a function.
Vue GWT manage this for you and pass a function so that all your Components have separate data models.
When parsing your the Component class we build a dataObject
Object for you. For example a Component like this:
@Component
public class StarkComponent implements IsVueComponent {
@Data String winter;
@Data boolean is;
@Data String coming;
}
Will result in the following dataObject
Object:
var dataObject = {
winter: null,
is: null,
coming: null
};
To get a new instance of this dataObject
Object to every Component, Vue GWT uses JSON.parse
and JSON.stringify
.
So the data
function passed to Vue.js looks like this:
var options = {
data: function () {
return JSON.parse(JSON.stringify(dataObject));
}
}
If you don't want the factory behavior on a Component for some reasons, you can pass the option useFactory to false
on your @Component
annotation.
The same dataObject
instance will be passed to all your instance of your Component and their data model will be shared.
Here is a working example:
@Component(useFactory = false)
public class SharedDataModelComponent implements IsVueComponent {
@Data int counter = 0;
}
<button @click="counter++">{{ counter }}</button>
We then instantiate 3 of those Components:
# Composing Components
Components are meant to be used together, most commonly in parent-child relationships: component A may use component B in its own template. They inevitably need to communicate to one another: the parent may need to pass data down to the child, and the child may need to inform the parent of something that happened in the child. However, it is also very important to keep the parent and the child as decoupled as possible via a clearly-defined interface. This ensures each component's code can be written and reasoned about in relative isolation, thus making them more maintainable and potentially easier to reuse.
In Vue, the parent-child component relationship can be summarized as props down, events up. The parent passes data down to the child via props, and the child sends messages to the parent via events. Let's see how they work next.
# Props
# Passing Data with Props
Every component instance has its own isolated scope. This means you cannot (and should not) directly reference parent data in a child component's template. Data can be passed down to child components using props.
A prop is a custom attribute for passing information from parent components.
A child component needs to explicitly declare the props it expects to receive using the @Prop
annotation:
@Component
public class ChildComponent implements IsVueComponent {
@Prop String message;
}
Then we can pass a plain string to it like so:
<child message="Hello World!"></todo>
# camelCase vs. kebab-case
HTML attributes are case-insensitive, so when using non-string templates, camelCased prop names need to use their kebab-case (hyphen-delimited) equivalents:
@Component
public class ChildComponent implements IsVueComponent {
@Prop String myMessage;
}
<!-- kebab-case in HTML -->
<child my-message="Hello World!"></child>
# Dynamic Props
Similar to binding a normal attribute to an expression, we can also use v-bind
for dynamically binding props to data on the parent.
Whenever the data is updated in the parent, it will also flow down to the child:
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
It's often simpler to use the shorthand syntax for v-bind
:
<child :my-message="parentMsg"></child>
TIP
This binding is validated at compile time as long as your child component is registered locally and not globally.
# Literal vs. Dynamic
A common mistake beginners tend to make is attempting to pass down a number using the literal syntax:
<!-- this passes down a plain string "1" -->
<comp some-prop="1"></comp>
However, since this is a literal prop, its value is passed down as a plain string "1"
instead of an actual number.
If we want to pass down an actual Number, we need to use v-bind
so that its value is evaluated as a Java expression:
<!-- this passes down an actual number -->
<comp v-bind:some-prop="1"></comp>
# One-Way Data Flow
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This prevents child components from accidentally mutating the parent's state, which can make your app's data flow harder to reason about.
In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console.
There are usually two cases where it's tempting to mutate a prop:
The prop is used to only pass in an initial value, the child component simply wants to use it as a local data property afterwards;
The prop is passed in as a raw value that needs to be transformed.
The proper answer to these use cases are:
1. Define a local data property that uses the prop's initial value as its initial value:
@Component
public class MyComponent implements IsVueComponent, HasCreated {
@Prop String initialCounter;
@Data String counter;
@Override
public void created() {
this.counter = this.initialCounter;
}
}
2. Define a computed property that is computed from the prop's value:
@Component
public class MyComponent implements IsVueComponent {
@Prop String size;
@Computed
public String getNormalizedSize() {
return this.size.trim().toLowerCase();
}
}
TIP
Note that objects and arrays in JavaScript (like in Java) are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child will affect parent state.
# Prop Validation
In Vue.js, it is possible for a component to specify requirements for the props it is receiving. If a requirement is not met, Vue will emit warnings. This is especially useful when you are authoring a component that is intended to be used by others.
Vue GWT can tell Vue.js to check the type based on the type of your Java property.
If you want this behavior you can pass checkType
to true to your @Prop
annotation.
When prop validation fails, Vue will produce a console warning (if using the development build).
It is also possible to have a custom validation method for your properties.
For this you can use the @PropValidator
annotation like this:
@Component
public class WhiteWalkerArmyComponent implements IsVueComponent {
@Prop int armyCount;
@PropValidator("armyCount")
boolean armyCountValidator(int value) {
// This will make a runtime error if the value passed for armyCount is <= 20000
return value > 20000;
}
}
# Prop Default Value
It is possible to set a default value for your Prop. This value will be set whenever the value has not been set by the parent. This mean whenever the Parent didn't add the property on the element in it's template.
To set this default value, you must create a method that returns it and annotate it with @PropDefault
:
@Component
public class PropDefaultValueComponent implements IsVueComponent {
@Prop String stringProp;
@PropDefault("stringProp")
String stringPropDefault() {
return "Hello World";
}
}
Beware that in this method you don't have access to your Instance (this
).
# Non-Prop Attributes
A non-prop attribute is an attribute that is passed to a component, but does not have a corresponding prop defined.
While explicitly defined props are preferred for passing information to a child component, authors of component libraries can't always foresee the contexts in which their components might be used. That's why components can accept arbitrary attributes, which are added to the component's root element.
For example, imagine we're using a 3rd-party bs-date-input
component with a Bootstrap plugin that requires a data-3d-date-picker
attribute on the input
.
We can add this attribute to our component instance:
<bs-date-input data-3d-date-picker="true"></bs-date-input>
And the data-3d-date-picker="true"
attribute will automatically be added to the root element of bs-date-input
.
# Replacing/Merging with Existing Attributes
Imagine this is the template for bs-date-input
:
<input type="date" class="form-control">
To add specify a theme for our date picker plugin, we might need to add a specific class, like this:
<bs-date-input
data-3d-date-picker="true"
class="date-picker-theme-dark"
></bs-date-input>
In this case, two different values for class
are defined:
form-control
, which is set by the component in its templatedate-picker-theme-dark
, which is passed to the component by its parent
For most attributes, the value provided to the component will replace the value set by the component.
So for example, passing type="large"
will replace type="date"
and probably break it!
Fortunately, the class
and style
attributes are a little smarter, so both values are merged, making the final value: form-control date-picker-theme-dark
.
# Custom Events
We have learned that the parent can pass data down to the child using props, but how do we communicate back to the parent when something happens? This is where Vue's custom event system comes in.
# Using v-on
with Custom Events
Every Vue instance implements an events interface, which means it can:
- Listen to an event using
$on(eventName)
- Trigger an event using
$emit(eventName)
TIP
Note that Vue's event system is separate from the browser's EventTarget API.
Though they work similarly, $on
and $emit
are not aliases for addEventListener
and dispatchEvent
.
In addition, a parent component can listen to the events emitted from a child component using v-on
directly in the template where the child component is used.
TIP
You cannot use $on
to listen to events emitted by children. You must use v-on
directly in the template, as in the example below.
Here's an example:
<button v-on:click="increment">{{ counter }}</button>
@Component
public class ButtonCounterComponent implements IsVueComponent {
@Data int counter = 0;
@JsMethod
public void increment() {
this.counter++;
// When incrementing, we fire an event.
vue().$emit("increment");
}
}
<div>
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
@Component(components = {ButtonCounterComponent.class})
public class CounterWithEventComponent implements IsVueComponent {
@Data int total = 0;
@JsMethod
public void incrementTotal() {
this.total++;
}
}
In this example, it's important to note that the child component is still completely decoupled from what happens outside of it. All it does is report information about its own activity, just in case a parent component might care.
# Passing a Value In Events
You can pass a value with your event. This value will be passed to the method that catch the event in the parent.
For example, let's change our counter component to pass the value of it's counter.
@Component
public class ButtonCounterComponent implements IsVueComponent {
@Data int counter = 0;
@JsMethod
public void increment() {
this.counter++;
// Pass the current value of the counter with the event.
vue().$emit("increment", this.counter);
}
}
We can now get this value in the parent:
<!-- Nothing changes in the template, we still pass the name of the method that handle the event -->
<div>
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
@Component(components = {ButtonCounterComponent.class})
public class CounterWithEventComponent implements IsVueComponent {
@Data int total = 0;
// But we can now get the value of the event as parameter
@JsMethod
public void incrementTotal(int childCounterValue) {
this.total += childCounterValue;
}
}
You don't have to catch the event value in the parent. If the parent method doesn't have the parameter, it will work without problems.
You can also alter the event value in your template before the method from your parent component is called. In that case, you must cast $event to it's type, so that Vue GWT knows what type to expect.
<div>
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal(((int) $event) * 2)"></button-counter>
<button-counter v-on:increment="incrementTotal(((int) $event) + 4)"></button-counter>
</div>
# @Emit
annotation
If you want a method to automatically emit an event each time it's called you can also use the @Emit
annotation.
It works similarly to the one from vue-property-decorator.
@Component
public class EmitAnnotationComponent implements IsVueComponent {
@Emit
@JsMethod
public void doSomething() {
DomGlobal.console.log("Doing something");
// Implicit call to vue().$emit("do-something")
}
@Emit
@JsMethod
public void doSomethingWithValue(int value) {
DomGlobal.console.log("Doing something with a value");
// Implicit call to vue().$emit("do-something-with-value", value)
}
@Emit("custom-event-name")
@JsMethod
public void doSomethingWithValueAndCustomName(int value) {
DomGlobal.console.log("Doing something");
// Implicit call to vue().$emit("custom-event-name", value)
}
}
# Binding Native Events to Components
There may be times when you want to listen for a native event on the root element of a component.
In these cases, you can use the .native
modifier for v-on
. For example:
<my-component v-on:click.native="doTheThing"></my-component>
# .sync
Modifier
2.3.0+
In some cases, we may need “two-way binding” for a prop. Unfortunately, true two-way binding can create maintenance issues, because child components can mutate the parent without the source of that mutation being obvious in both the parent and the child.
That’s why instead, we recommend emitting events in the pattern of update:myPropName. For example, in a hypothetical component with a title prop, we could communicate the intent of assigning a new value with:
vue().$emit('update:title', newTitle)
<text-document
v-bind:title="title"
v-on:update:title="title = $event"
></text-document>
For convenience, we offer a shorthand for this pattern with the .sync modifier:
<text-document v-bind:title.sync="title"></text-document>
WARNING
It's important to note that only @Data
fields or @Computed
with a getter and a setter can be used directly with .sync
in Vue GWT.
Apart from this limitations, .sync
works the same way in Vue GWT than in Vue.js.
# Form Input Components using Custom Events
Custom events can also be used to create custom inputs that work with v-model
. Remember:
<input v-model="something">
is just syntactic sugar for:
<input
v-bind:value="something"
v-on:input="setSomethingValue((Event) $event)">
When used with a component, this simplifies to:
<custom-input
:value="something"
@input="something = (Type) $event">
</custom-input>
So for a component to work with v-model
, it should (these can be configured in 2.2.0+):
- accept a
value
prop - emit an
input
event with the new value
# Customizing Component v-model
Vue.js support customizing component v-model
.
This is not yet supported by Vue GWT.
# Non Parent-Child Communication
Sometimes two components may need to communicate with one-another but they are not parent/child to each other.
For this you can use a GWT EventBus.
In more complex cases, you should consider employing a dedicated state-management pattern.
# Content Distribution with Slots
When using components, it is often desired to compose them like this:
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>
There are two things to note here:
The
<app>
component does not know what content it will receive. It is decided by the component using<app>
.The
<app>
component very likely has its own template.
To make the composition work, we need a way to interweave the parent "content" and the component's own template.
This is a process called content distribution (or "transclusion" if you are familiar with Angular).
Vue.js implements a content distribution API that is modeled after the current Web Components spec draft, using the special <slot>
element to serve as distribution outlets for the original content.
# Compilation Scope
Before we dig into the API, let's first clarify which scope the contents are compiled in. Imagine a template like this:
<child-component>
{{ message }}
</child-component>
Should the message
be bound to the parent's data or the child data?
The answer is the parent.
A simple rule of thumb for component scope is:
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in child scope.
A common mistake is trying to bind a directive to a child property/method in the parent template:
<!-- does NOT work -->
<child-component v-show="someChildProperty"></child-component>
Assuming someChildProperty
is a property on the child component, the example above would not work.
The parent's template is not aware of the state of a child component.
If you need to bind child-scope directives on a component root node, you should do so in the child component's own template:
@Component
public class ChildComponent implements IsVueComponent {
@Data boolean someChildProperty;
}
<div v-show="someChildProperty">Child</div>
Similarly, distributed content will be compiled in the parent scope.
# Single Slot
Parent content will be discarded unless the child component template contains at least one <slot>
outlet.
When there is only one slot with no attributes, the entire content fragment will be inserted at its position in the DOM, replacing the slot itself.
Anything originally inside the <slot>
tags is considered fallback content.
Fallback content is compiled in the child scope and will only be displayed if the hosting element is empty and has no content to be inserted.
Suppose we have a component called my-component
with the following template:
<div>
<h2>I'm the child title</h2>
<slot>
This will only be displayed if there is no content
to be distributed.
</slot>
</div>
And a parent that uses the component:
<div>
<h1>I'm the parent title</h1>
<my-component>
<p>This is some original content</p>
<p>This is some more original content</p>
</my-component>
</div>
The rendered result will be:
<div>
<h1>I'm the parent title</h1>
<div>
<h2>I'm the child title</h2>
<p>This is some original content</p>
<p>This is some more original content</p>
</div>
</div>
# Named Slots
<slot>
elements have a special attribute, name
, which can be used to further customize how content should be distributed.
You can have multiple slots with different names.
A named slot will match any element that has a corresponding slot
attribute in the content fragment.
There can still be one unnamed slot, which is the default slot that serves as a catch-all outlet for any unmatched content. If there is no default slot, unmatched content will be discarded.
For example, suppose we have an app-layout
component with the following template:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
Parent markup:
<app-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</app-layout>
The rendered result will be:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
The content distribution API is a very useful mechanism when designing components that are meant to be composed together.
# Scoped Slots
Sometimes you'll want to provide a component with a reusable slot that can access data from the child component.
For example, a simple <todo-list>
component may contain the following in its template:
<vue-gwt:import class="com.mypackage.Todo"/>
<ul>
<li
v-for="Todo todo in todos"
v-bind:key="todo.getId()">
{{ todo.getText() }}
</li>
</ul>
But in some parts of our app, we want the individual todo items to render something different than just todo.getText()
.
This is where scoped slots come in.
To make the feature possible, all we have to do is wrap the todo item content in a <slot>
element, then pass the slot any data relevant to its context: in this case, the todo
object:
<vue-gwt:import class="com.mypackage.Todo"/>
<ul>
<li
v-for="Todo todo in todos"
v-bind:key="todo.getId()">
<!-- We have a slot for each todo, passing it the -->
<!-- `todo` object as a slot prop. -->
<slot v-bind:todo="todo">
<!-- Fallback content -->
{{ todo.getText() }}
</slot>
</li>
</ul>
Now when we use the <todo-list>
component, we can optionally define an alternative <template>
for todo items, but with access to data from the child via the slot-scope
attribute:
<vue-gwt:import class="com.mypackage.Todo"/>
<todo-list v-bind:todos="todos">
<!-- Define a variable todo that will get the value of the binded attribute todo -->
<template v-slot="{ Todo todo }">
<!-- Define a custom template for todo items, using -->
<!-- `slotProps` to customize each todo. -->
<span v-if="todo.isComplete()">✓</span>
{{ todo.getText() }}
</template>
</todo-list>
If you pass several values on your slot, just list them like so: slot-scope="{ Todo todo, int count }"
.
TIP
You may notice that wrap the list of variables in {}
.
This is because Vue.js actually pass us a JsPropertyMap
, where the keys are the names of the properties and the values are the values that are binded.
Vue GWT manages destructuring this JsPropertyMap
into variables for you.
If you want to access the actual slot-scope object passed from Vue.js, use: slot-scope="slotScope"
.
# Dynamic Components
You can use the same mount point and dynamically switch between multiple components using the reserved <component>
element and dynamically bind to its is
attribute:
@Component(components = { TargaryenComponent.class, StarkComponent.class, LannisterComponent.class })
public class HousesComponent implements IsVueComponent {
@Data String currentHouse = "targaryen";
}
<component v-bind:is="currentHouse">
<!-- component changes when vm.currentHouse changes! -->
</component>
# keep-alive
If you want to keep the switched-out components in memory so that you can preserve their state or avoid re-rendering, you can wrap a dynamic component in a <keep-alive>
element:
<keep-alive>
<component :is="currentView">
<!-- inactive components will be cached! -->
</component>
</keep-alive>
Check out more details on <keep-alive>
in the API reference.
# Misc
# Authoring Reusable Components
When authoring components, it's good to keep in mind whether you intend to reuse it somewhere else later. It's OK for one-off components to be tightly coupled, but reusable components should define a clean public interface and make no assumptions about the context it's used in.
The API for a Vue component comes in three parts - props, events, and slots:
Props allow the external environment to pass data into the component
Events allow the component to trigger side effects in the external environment
Slots allow the external environment to compose the component with extra content.
With the dedicated shorthand syntax for v-bind
and v-on
, the intents can be clearly and succinctly conveyed in the template:
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat"
>
<template v-slot:icon>
<img src="...">
</template>
<template v-slot:main-text>
<p>Hello!</p>
</template>
</my-component>
# Child Component Refs
Despite the existence of props and events, sometimes you might still need to directly access a child component in Java.
To achieve this you have to assign a reference ID to the child component using ref
. For example:
<div id="parent">
<user-profile ref="profile"></user-profile>
</div>
@Component(components = UserProfileComponent.class)
public class ParentComponent implements IsVueComponent, HasCreated {
@Ref
UserProfileComponent profile;
@Override
public void created() {
profile.doSomething();
}
}
When ref
is used together with v-for
, the ref you get will be an array so your @Ref
field must be a JsArray<MyComponent>
.
Vue GWT will check that for you at compile time.
TIP
@Ref
are only populated after the component has been rendered, and they are not reactive.
They are only meant as an escape hatch for direct child manipulation - you should avoid using @Ref
in templates or computed properties.
# Async Components
Vue.js supports dynamically loading components with caching. This is not yet supported by Vue GWT.
# Recursive Components
Components can recursively invoke themselves in their own template.
However, they can only do so with the name
option:
@Component(name = "unique-name-of-my-component")
When you register a component globally using Vue.component
, the global ID is automatically set as the component's name
option.
Vue.component("unique-name-of-my-component", MyComponentFactory.get());
If you're not careful, recursive components can also lead to infinite loops:
@Component(name = "stack-overflow")
<div><stack-overflow></stack-overflow></div>
A component like the above will result in a "max stack size exceeded" error, so make sure recursive invocation is conditional (i.e. uses a v-if
that will eventually be false
).
Bellow is an example recursive component:
@Component(name = "recursive")
public class RecursiveComponent implements IsVueComponent, HasCreated {
@Prop Integer counter;
@Override
public void created() {
if (this.counter == null)
this.counter = 0;
}
}
<span>
{{ counter }}
<recursive v-if="counter < 5" :counter="counter + 1"></recursive>
</span>
# Circular References Between Components
Let's say you're building a file directory tree, like in Finder or File Explorer.
You might have a tree-folder
component with this template:
<p>
<span>{{ myFolder.name }}</span>
<tree-folder-content v-if="myFolder.hasContent()" :content="myFolder.getContent()"/>
</p>
Then a tree-folder-contents
component with this template:
<vue-gwt:import class="com.mypackage.Folder"/>
<ol>
<li v-for="Folder child in content">
<tree-folder :folder="child"/>
</li>
</ol>
When you look closely, you'll see that these components will actually be each other's descendent and ancestor in the render tree - a paradox!
In Vue GWT (like in Vue.js) this paradox is solved automatically, whether you declare your components globally or by passing them to the @Component
annotation.
Here is a working and running tree example for you:
# Inline Templates
Vue.js support inline templates. This is not supported by Vue GWT.
# X-Templates
Vue.js support X-Templates. This is not supported by Vue GWT.
# Cheap Static Components with v-once
Rendering plain HTML elements is very fast in Vue, but sometimes you might have a component that contains a lot of static content.
In these cases, you can ensure that it's only evaluated once and then cached by adding the v-once
directive to the root element, like this:
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>