title | excerpt | url |
---|---|---|
Components |
Learn more about Jovo Components, which are self-contained and reusable elements in a Jovo app. |
Components are self-contained and reusable elements in a Jovo app that handle the conversational flow. Similar to web frameworks like Vue and React, Jovo allows you to build complex applications composed of components of varying sizes.
You can see a component as an isolated part of your app that handles a specific task. It could be something small like asking for a confirmation (yes or no), and something bigger like collecting all necessary information for a restaurant table reservation. For larger cases like the latter example, it's also possible for a component to have multiple subcomponents.
📦src
┣ 📂components
┃ ┣ 📜GlobalComponent.ts
┃ ┣ 📜LoveHatePizzaComponent.ts
┃ ┗ ...
Components are located in the src/components
folder of a Jovo app. While a component can be a complete folder (that may contain its own output classes, subcomponents, and more), the most common approach to get started is to have single component classes.
For example, the Jovo v4
template contains a GlobalComponent
that looks like this:
// src/components/GlobalComponent.ts
import { Component, BaseComponent, Global } from '@jovotech/framework';
import { LoveHatePizzaComponent } from './LoveHatePizzaComponent';
/*
|--------------------------------------------------------------------------
| Global Component
|--------------------------------------------------------------------------
|
| The global component handlers can be reached from anywhere in the app
| Learn more here: www.jovo.tech/docs/components#global-components
|
*/
@Global()
@Component()
export class GlobalComponent extends BaseComponent {
LAUNCH() {
return this.$redirect(LoveHatePizzaComponent);
}
}
The component class section dives deeper into the contents of the example above.
Root components like GlobalComponent
are registered in the app.ts
(example) like this:
import { GlobalComponent } from './components/GlobalComponent';
import { LoveHatePizzaComponent } from './components/LoveHatePizzaComponent';
// ...
const app = new App({
/*
|--------------------------------------------------------------------------
| Components
|--------------------------------------------------------------------------
|
| Components contain the Jovo app logic
| Learn more here: www.jovo.tech/docs/components
|
*/
components: [GlobalComponent, LoveHatePizzaComponent],
});
Learn more about components in the sections below:
- Component class: How files like
YourComponent.ts
are structured - Component registration: How components are added to your app
- Advanced component features
Each component consists of at least a component class that is imported in the app configuration (for root components) or a parent component class (for subcomponents). The smallest possible component class could look like this:
// src/components/YourComponent.ts
import { Component, BaseComponent } from '@jovotech/framework';
@Component()
class YourComponent extends BaseComponent {
START() {
// ...
}
}
It includes the following elements:
@Component()
decorator: This is used to mark this class as a component. It can also include component options.- Handlers like
START
: These handlers contain the dialogue logic of the component.
Besides those, the following stages also dive into the following concepts:
The core of a component class are handlers that are responsible for responding to a request and returning output.
For example, the LoveHatePizzaComponent
in the Jovo v4
template includes the following handlers:
@Component()
export class LoveHatePizzaComponent extends BaseComponent {
START() {
return this.$send(YesNoOutput, { message: 'Do you like pizza?' });
}
@Intents(['YesIntent'])
lovesPizza() {
return this.$send({ message: 'Yes! I love pizza, too.', listen: false });
}
@Intents(['NoIntent'])
hatesPizza() {
return this.$send({ message: `That's OK! Not everyone likes pizza.`, listen: false });
}
UNHANDLED() {
return this.START();
}
}
Learn more about handlers here.
If you're used to building state machines (for example Jovo v3
), you can see a Jovo component as a state.
Once a component is entered, it is added to the Jovo $state
stack:
$state = [
{
component: 'SomeComponent',
},
];
The component is removed from the stack once it resolves or the session closes.
There are two ways how a component can be entered:
- Through one of its global handlers (or any handlers if it's a global component)
- By getting called from a different component using
$redirect()
or$delegate()
You can find out more about the $state
stack here and learn about component delegation in our handlers documentation.
A Jovo project usually comes with a GlobalComponent
. This (and potentially other components) is a special global
component that has the following characteristics:
- Each of its handlers is global, no need to add a
global
property. - It does not get added to the
$state
stack (except it uses$delegate()
, then it is added to the stack just until the delegation was resolved). - It does not store component data: If you want to store data, we recommend using session data.
subState
does not work. We recommend using$delegate()
.
You can either add the global
property to the component options:
@Component({ global: true /* other options */ })
class YourComponent extends BaseComponent {
// ...
}
Or use the @Global
convenience decorator:
@Global()
@Component({
/* options */
})
class YourComponent extends BaseComponent {
// ...
}
As a rule of thumb, a global component can be seen as a "last resort" that is only triggered if no other more specific component matches a request. Global LAUNCH
, UNHANDLED
, or a help handler are often part of a global component.
For data that is only relevant for this specific component, you can use component data:
this.$component.data.someKey = 'someValue';
This is then added to the $state
stack and lost once the component resolves:
$state = [
{
component: 'SomeComponent',
data: {
someKey: 'someValue',
},
},
];
Global components don't store component data because they're not added to the $state
stack. We recommend using session data instead. Learn more about the different Jovo data types here.
For type safety, you can also add an interface that extends ComponentData
:
import { Component, BaseComponent, ComponentData } from '@jovotech/framework';
export interface YourComponentData extends ComponentData {
someKey: string;
}
class YourComponent extends BaseComponent<YourComponentData> {
// ...
}
For some components, it may be helpful (or necessary) to add options for customization or configuration. The following options can be added:
components
: Subcomponents that are used by this component.config
: The custom config used by the component. Can be accessed withthis.$component.config
.name
: If two components have the same class name, one component's name can be changed here.isAvailable
: A function that returns a boolean whether the component is available. If it returnsfalse
, the component is skipped during routing and redirecting or delegating to it causes aComponentNotAvailableError
.
In the register root components section, we already talked about how to pass options when registering existing components.
It is also possible to add options to a component class using its @Component
decorator:
@Component({
/* options */
})
class YourComponent extends BaseComponent {
// ...
}
For type safety, you can also add an interface that extends ComponentConfig
:
import { Component, BaseComponent, ComponentConfig } from '@jovotech/framework';
export interface YourComponentConfig extends ComponentConfig {
someKey: string;
}
class YourComponent extends BaseComponent<YourComponentConfig> {
// ...
}
The hierarchy of options being used by the component is as follows (starting with the most important one):
- Options passed using the constructor when registering the component
- Options in the
@Component
decorator - Default options of the component
You can also add a constructor()
to your component:
import { Jovo, UnknownObject, Component, BaseComponent } from '@jovotech/framework';
class YourComponent extends BaseComponent {
constructor(
jovo: Jovo,
options: UnknownObject | undefined
) {
super(jovo, options);
// ...
}
// ...
}
For the options
, it is important that you use UnknownObject | undefined
. This is a breaking change that was introduced with this PR in v4.5
.
If you run into errors, you can also try ComponentOptions<UnknownObject> | undefined
:
When we talk about components in this documentation, we typically talk about a specific component class. These classes can either be registered globally in the app.ts
file (root components) or as subcomponents of other component classes.
Root components are registered in the app.ts
file (or any other app config file). These are all top-level components that are accessible using global handlers.
Each Jovo template usually comes with a GlobalComponent
that is added like this:
// src/app.ts
import { GlobalComponent } from './components/GlobalComponent';
// ...
const app = new App({
// ...
components: [GlobalComponent],
// ...
});
You can add more components by importing their classes and referencing them in the components
array:
// src/app.ts
import { GlobalComponent } from './components/GlobalComponent';
import { YourComponent } from './components/YourComponent';
// ...
const app = new App({
// ...
components: [GlobalComponent, YourComponent],
// ...
});
Some components (especially from third parties) may require you to add options. Learn more about component options here.
There are two ways how you can add those to your root component registration:
- Using
ComponentDeclaration
(this will allow you to access the types of the component options) - Using an object
If you're a TypeScript user, we recommend using ComponentDeclaration
. This way, your code editor will be able to provide the option types with code completion:
// src/app.ts
import { ComponentDeclaration } from '@jovotech/framework';
import { YourComponent } from './components/YourComponent';
// ...
const app = new App({
// ...
components: [
new ComponentDeclaration(YourComponent, {
/* options */
}),
],
// ...
});
You can also use an object:
// src/app.ts
import { YourComponent } from './components/YourComponent';
// ...
const app = new App({
// ...
components: [
{
component: YourComponent,
options: {
/* options */
},
},
],
// ...
});
One example of an option is name
. If you use two components that have the same class name (especially relevant for third-party components), you can rename one and pass its adjusted name. In the below example, both imported files export a MenuComponent
class:
// src/app.ts
import { MenuComponent } from './components/MenuComponent';
import { MenuComponent as MenuComponent2 } from './components/MenuComponent2';
// ...
const app = new App({
// ...
components: [MenuComponent, new ComponentDeclaration(MenuComponent2, { name: 'MenuComponent2' })],
// ...
});
Subcomponents They are registered inside their parent component using the components
property in the @Component
decorator.
import { YourSubComponent } from './YourSubComponent';
// ...
@Component({
components: [YourSubComponent],
})
class YourComponent extends BaseComponent {
// ...
}
You can pass the following interfaces and types to BaseComponent
:
- Component options
- Component data
- Component events when delegating to a component
For example, this could look like this:
import { Component, BaseComponent, ComponentConfig, ComponentData } from '@jovotech/framework';
export interface YourComponentConfig extends ComponentConfig {
someKey: string;
}
export interface YourComponentData extends ComponentData {
someKey: string;
}
export enum YourComponentEvents {
Yes = 'onYes',
No = 'onNo',
}
class YourComponent extends BaseComponent<YourComponentData, YourComponentConfig, YourComponentEvents> {
// ...
}
You don't need to pass all three elements:
import { Component, BaseComponent } from '@jovotech/framework';
export enum YourComponentEvents {
Yes = 'onYes',
No = 'onNo',
}
class YourComponent extends BaseComponent<{}, {}, YourComponentEvents> {
// ...
}
The ComponentTree
contains all registered components:
this.$handleRequest.componentTree;
Here is an example tree:
{
"tree": {
"GlobalComponent": {
"path": ["GlobalComponent"],
"metadata": {
"options": {
"global": true
}
}
},
"LoveHatePizzaComponent": {
"path": ["LoveHatePizzaComponent"],
"metadata": {
"options": {}
}
}
}
}
You can also access the active component like this:
this.$handleRequest.activeComponentNode;
Components can inherit handlers from their superclass. This is useful for example if many of your components offer a similar workflow, like a help handler.
import { BaseComponent } from '@jovotech/framework';
abstract class ComponentWithHelp extends BaseComponent {
abstract showHelp(): Promise<void>;
async repeatLastResponse() {
// ...
}
@Intents('HelpIntent')
async help() {
await this.showHelp();
await this.repeatLastResponse();
}
}
@Component()
class YourComponent extends ComponentWithHelp {
async showHelp() {
// ...
}
}
When using inheritance, the following rules apply:
- The subclass has to be annotated with
@Component
and registered in app. If the superclass is annotated with@Component
, any options provided there will be ignored. - Handlers in the subclass will override handlers and their decorators in the superclass. This means that when overriding a handler for a specific intent, it will have to be annotated with
@Intents
/@Handle
again.