Skip to content

Latest commit

 

History

History
547 lines (396 loc) · 16.7 KB

components.md

File metadata and controls

547 lines (396 loc) · 16.7 KB
title excerpt url
Components
Learn more about Jovo Components, which are self-contained and reusable elements in a Jovo app.

Components

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.

Introduction

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

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:

Handlers

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.

Routing and State Management

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:

You can find out more about the $state stack here and learn about component delegation in our handlers documentation.

Global Components

A Jovo project usually comes with a GlobalComponent. This (and potentially other components) is a special global component that has the following characteristics:

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.

Component Data

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> {
  // ...
}

Component Options

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 with this.$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 returns false, the component is skipped during routing and redirecting or delegating to it causes a ComponentNotAvailableError.

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

Component Constructor

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:

Component Registration

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.

Register Root Components

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' })],

  // ...
});

Register Subcomponents

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 {
  // ...
}

Advanced Component Features

Type Safety

You can pass the following interfaces and types to BaseComponent:

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> {
  // ...
}

ComponentTree

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;

Inheritance

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.