Kogito Tooling Examples —How to create a custom View

Photograph by Cristina Mittermeier

This is part of a series of blog posts, and this one covers “How to create a custom View”.

You can navigate through the series by clicking on the following topics:

In this section, we’re going to cover how to create a custom View. If you have questions about what is a View (read more about it here), but to summarize it, a View is an Envelope. Or, to be more specific, an iframe element with your code inside and a strongly-typed communication interface with the Channel.

Like our previous sections, we’re going to utilize some React concepts to create and utilize Views (especially React hooks), so in case you’re not familiar with it, it’s suggested that you read the React documentation.

Starting

A custom View has three essential “submodules”, which are separated in directories:

  • api: the API definition, which is used to communicate with the Channel.
  • embedded: the Embedded Envelope component implementation, which is used by the Channel. (this is specific to React)
  • envelope: this custom View’s Envelope implementation.

In this first example, we’ll create a ‘To-do’ List View, which will enable the Channel to use this View at any place. This View will feature adding, removing and marking items as complete.

API

The ‘To-do’ List View api submodule has the following files:

TodoListViewApi.ts

This is the API of your View as a component. It exposes methods that allow you to control your View component.

import { Item } from "./TodoListEnvelopeApi";
export interface TodoListApi {
addItem(item: string): Promise<void>;
getItems(): Promise<Item[]>;
markAllAsCompleted(): void;
}
view raw TodoListApi.ts hosted with ❤ by GitHub

TodoListViewChannelApi.ts

This is the API of a Channel where this View’s Envelope can run on, and it’s consumed by the Envelope.

export interface TodoListChannelApi {
todoList__itemRemoved(item: string): void;
}

TodoListViewEnvelopeApi.ts

And at least, this is the API of your component’s Envelope, which is consumed by the Channel.

export interface TodoListEnvelopeApi {
todoList__init(association: Association, initArgs: TodoListInitArgs): Promise<void>;
todoList__addItem(item: string): Promise<void>;
todoList__getItems(): Promise<Item[]>;
todoList__markAllAsCompleted(): void;
}
export interface Association {
origin: string;
envelopeServerId: string;
}
export interface TodoListInitArgs {
user: string;
}
export interface Item {
label: string;
completed: boolean;
}

Embedded

This is a convenience submodule specific for React. Here is the implementation of the EmbeddedTodoList. It’s a component used by the Channel, which makes the communication between the Channel and the Envelope. Hence, it connects the interfaces from the api directory. To create this component, we’ll utilize the EmbeddedEnvelope component provided by Kogito Tooling.

RefForwardingComponent

To start, we’ll use a RefForwardingComponent which provides a way to access the component imperatively and accessing its exposed methods. Also, it enables the usage of React hooks.

import { TodoListApi, TodoListChannelApi } from "../api";
export type Props = TodoListChannelApi & {
targetOrigin: string;
envelopePath: string;
};
export const EmbeddedTodoList = React.forwardRef<TodoListApi, Props>((props, forwardedRef) => {
// …
});

Here the forwardedRef implements the TodoListApi. The props implement the TodoListChannelApi with two new attributes, which are used to tell the EmbeddedEnvelope component where the Envelope is located.

PoolInit

To start the communication between Channel and Envelope, we need to create a method used on the initialization. This method is called poollInit, and it receives an EnvelopeServer instance typed with the Channel and Envelope API’s. The EnvelopeServer is responsible for sending messages from the Channel to the Envelope (strictly in this direction), and receiving messages from the Envelope too. The pollInit method tells the EnvelopeServer what to do to “connect” with the Envelope. It can contain any parameters and this method can be called many times, since the Envelope can take a while to load. Beware of side-effects!

import { EnvelopeServer } from "@kogito-tooling/envelope-bus/dist/channel";
import { TodoListChannelApi, TodoListEnvelopeApi } from "../api";
export const EmbeddedTodoList = React.forwardRef<TodoListApi, Props>((props, forwardedRef) => {
const pollInit = useCallback((envelopeServer: EnvelopeServer<TodoListChannelApi, TodoListEnvelopeApi>) => {
return envelopeServer.envelopeApi.requests.todoList__init(
{
origin: envelopeServer.origin,
envelopeServerId: envelopeServer.id,
},
{ user: "Tiago" }
);
}, []);
// …
});

refDelegate

The methods implemented on the TodoListApi interface are going to be exposed through this component reference. In this example, the EnvelopeServer has the implementation of the methods. To abstract the EnvelopeServer instantiation, we delegate the exposure of the component reference to the EmbeddedEnvelopeFactory, which already has an EnvelopeServer instance, making the EmbeddedTodoList a lot simpler.

import { EnvelopeServer } from "@kogito-tooling/envelope-bus/dist/channel";
import { TodoListApi, TodoListChannelApi, TodoListEnvelopeApi } from "../api";
export const EmbeddedTodoList = React.forwardRef<TodoListApi, Props>((props, forwardedRef) => {
// …
const refDelegate = useCallback(
(envelopeServer: EnvelopeServer<TodoListChannelApi, TodoListEnvelopeApi>): TodoListApi => ({
addItem: (item) => envelopeServer.envelopeApi.requests.todoList__addItem(item),
getItems: () => envelopeServer.envelopeApi.requests.todoList__getItems(),
markAllAsCompleted: () => envelopeServer.envelopeApi.notifications.todoList__markAllAsCompleted(),
}),
[]
);
// …
});

EmbeddedEnvelope

To create an EmbeddedEnvelope component, we utilize an available factory from Kogito Tooling, which will create the component, initializing the EnvelopeServer with the pollInit and then exposing the methods on the TodoListApi interface through the refDelegate method.

import { TodoListApi } from "../api";
import { EmbeddedEnvelopeFactory } from "@kogito-tooling/envelope/dist/embedded";
export const EmbeddedTodoList = React.forwardRef<TodoListApi, Props>((props, forwardedRef) => {
// …
const EmbeddedEnvelope = useMemo(() => {
return EmbeddedEnvelopeFactory({
api: props,
envelopePath: props.envelopePath,
origin: props.targetOrigin,
refDelegate,
pollInit,
});
}, []);
return <EmbeddedEnvelope ref={forwardedRef} />;
});

Envelope

All the code that runs inside the Envelope is here. The ‘To-do’ List Envelope is separated in:

  • TodoListEnvelope.tsx
  • TodoListEnvelopeApiImpl.tsx
  • TodoListEnvelopeContext.tsx
  • TodoListEnvelopeView.tsx

TodoListEnvelope

This file has the function responsible for initializing the Envelope. To start, it will create an Envelope with the ‘To-do’ List types.

import { EnvelopeBus } from "@kogito-tooling/envelope-bus/dist/api";
import { Envelope } from "@kogito-tooling/envelope";
import { TodoListEnvelopeContext } from "./TodoListEnvelopeContext";
import { TodoListEnvelopeApiImpl } from "./TodoListEnvelopeApiImpl";
import { TodoListChannelApi, TodoListEnvelopeApi } from "../api";
export function init(args: { container: HTMLElement; bus: EnvelopeBus }) {
const envelope = new Envelope<
TodoListEnvelopeApi,
TodoListChannelApi,
TodoListEnvelopeViewApi,
TodoListEnvelopeContext
>(args.bus);
// …
}

Now, we create a method that knows how to render a TodoListEnvelopeView and returns a function with its API. In this example, we’re using a React View, but it’s possible to use any other framework here or just plain Javascript, giving flexibility to your implementation.

import { TodoListEnvelopeView, TodoListEnvelopeViewApi } from "./TodoListEnvelopeView";
export function init(args: { container: HTMLElement; bus: EnvelopeBus }) {
// …
const envelopeViewDelegate = async () => {
const ref = React.createRef<TodoListEnvelopeViewApi>();
return new Promise<() => TodoListEnvelopeViewApi>((res) =>
ReactDOM.render(<TodoListEnvelopeView ref={ref} channelApi={envelope.channelApi} />, args.container, () =>
res(() => ref.current!)
)
);
};
// …
}

To finalize, we start the Envelope passing the View, the context, and a factory, which instantiates an Envelope API. In this example, our factory is not generic, and can only instantiate the same Envelope Api, but further, in another example, we’re going to see a more advanced factory implementation. A context is an empty object because of this example’s simplicity, but you can check it out an implementation on the Kogito Tooling for a more advanced implementation.

import { EnvelopeBus } from "@kogito-tooling/envelope-bus/dist/api";
import { TodoListEnvelopeApiImpl } from "./TodoListEnvelopeApiImpl";
import { TodoListEnvelopeContext } from "./TodoListEnvelopeContext";
export function init(args: { container: HTMLElement; bus: EnvelopeBus }) {
// …
const context: TodoListEnvelopeContext = {};
return envelope.start(envelopeViewDelegate, context, {
create: (apiFactoryArgs) => new TodoListEnvelopeApiImpl(apiFactoryArgs),
});
}

TodoListEnvelopeApiImpl

This file has the implementation of the Envelope API, and it receives an EnvelopeApiFactoryArgs. It has access to the View API, the EnvelopeContext, and the EnvelopeBusController. The EnvelopeBusController is the class responsible for the communication from the Envelope to the Channel (strictly this flow of messages).

import { EnvelopeApiFactoryArgs } from "@kogito-tooling/envelope";
import { TodoListEnvelopeContext } from "./TodoListEnvelopeContext";
import { TodoListEnvelopeViewApi } from "./TodoListEnvelopeView";
import { TodoListChannelApi, TodoListEnvelopeApi } from "../api";
export class TodoListEnvelopeApiImpl implements TodoListEnvelopeApi {
constructor(
private readonly args: EnvelopeApiFactoryArgs<
TodoListEnvelopeApi,
TodoListChannelApi,
TodoListEnvelopeViewApi,
TodoListEnvelopeContext
>
) {}
// …
}

The Envelope API implementation relies on the View API except for the todoList__init method. It will associate the actual Envelope with the Channel and set the user on our View (TodoListEnvelopeView).

import { Association, TodoListInitArgs } from "../api";
export class TodoListEnvelopeApiImpl implements TodoListEnvelopeApi {
// …
public async todoList__init(association: Association, initArgs: TodoListInitArgs) {
this.args.envelopeBusController.associate(association.origin, association.envelopeServerId);
this.args.view().setUser(initArgs.user);
}
public async todoList__addItem(item: string) {
return this.args.view().addItem(item);
}
public async todoList__getItems() {
return this.args.view().getItems();
}
public todoList__markAllAsCompleted() {
this.args.view().markAllAsCompleted();
}
}

TodoListEnvelopeView

This file has what is going to be rendered on the View. Also, it has the View API, which is consumed by the TodoListEnvelopeApiImpl file.

Like our other components in this post, we will be using the React RefForwardingComponent, and now we typed it with the TodoListEnvelopeViewApi. Additionally, we created an user state to have a more friendly interface, and an items state, which has the list of all items on our ‘To-do’ List.

import { MessageBusClientApi } from "@kogito-tooling/envelope-bus/dist/api";
import { useState } from "react";
import { Item, TodoListChannelApi } from "../api";
export interface TodoListEnvelopeViewApi {
setUser(user: string): void;
addItem(item: string): void;
getItems(): Item[];
markAllAsCompleted(): void;
}
interface Props {
channelApi: MessageBusClientApi<TodoListChannelApi>;
}
export const TodoListEnvelopeView = React.forwardRef<TodoListEnvelopeViewApi, Props>((props, forwardedRef) => {
const [user, setUser] = useState<string | undefined>();
const [items, setItems] = useState<Item[]>([]);
// …
});

Utilizing the userImperativeHandle we expose the TodoListEnvelopeViewApi methods.

import { useImperativeHandle } from "react";
export const TodoListEnvelopeView = React.forwardRef<TodoListEnvelopeViewApi, Props>((props, forwardedRef) => {
// …
useImperativeHandle(
forwardedRef,
() => ({
setUser,
addItem: (item) => setItems([items, { label: item, completed: false }]),
getItems: () => items,
markAllAsCompleted: () => setItems(items.map((item) => ({item, completed: true }))),
}),
[items]
);
// …
});

To manipulate our ‘To-do’ List View, we created some useful callbacks with the userCallback hook.

  • removeItem will create a copy of the items array, so it doesn’t modify the current state’s value. Using the copied array, it’ll search for the specified item, and then remove it by setting a state with the updated array (before the removal).
  • updateItemCompletedStatus will also create a copy of the current items array. It will search for the specified item and then update the state with the updated array.
  • allCompleted is a memoized value updated every time the items state is changed. It checks if all items have the completed property equals to true.
import { useCallback, useMemo } from "react";
import { Item, TodoListChannelApi } from "../api";
export const TodoListEnvelopeView = React.forwardRef<TodoListEnvelopeViewApi, Props>((props, forwardedRef) => {
// …
const removeItem = useCallback(
(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>, item: Item) => {
e.preventDefault();
const itemsCopy = [items];
const i = itemsCopy.indexOf(item);
if (i >= 0) {
itemsCopy.splice(i, 1);
setItems(itemsCopy);
props.channelApi.notifications.todoList__itemRemoved(item.label);
}
},
[items]
);
const updateItemCompletedStatus = useCallback(
(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>, item: Item, completed: boolean) => {
e.preventDefault();
const itemsCopy = [items];
const i = itemsCopy.indexOf(item);
if (i >= 0) {
itemsCopy[i].completed = completed;
setItems(itemsCopy);
}
},
[items]
);
const allCompleted = useMemo(() => {
const completedItems = items.filter((i) => i.completed);
return items.length > 0 && completedItems.length === items.length;
}, [items]);
// …
});

To finalize our View, the only missing part is what is going to be rendered. We’re not going to cover the specifics of this implementation because it involves personal choices. This is the look of the final implementation.

The ‘To-do’ List View with a user (“Tiago”) and two items that were manually created to exemplify how the View looks like

Wrapping up

Now you know how to create your own custom View. In the next section, we’ll see how to Embed it on a VS Code Extension.

Thanks for reading!

This post was original published on here.
0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments