Skip to main content

UI Component

While the app grows, we might want to reuse the UI to keep the codes DRY. In this lesson, we'll go deeper to make reusable chat UI components.

Time to accomplish: 15 minutes

Building Component

Let's continue creating the todos list UI. But this time, we'll build it in a customized JSX Component.

First add this type that represent the todo data:

src/types.ts
//...
export type Todo = {
id: number;
name: string;
};

Then create a src/components/TodoList.tsx file with the following content:

src/components/TodoList.tsx
import Machinat from '@machinat/core';
import * as Messenger from '@machinat/messenger/components';
import { Todo } from '../types';

type TodoListProps = {
todos: Todo[];
};

const TodoList = ({ todos }: TodoListProps, { platform }) => {
if (todos.length === 0) {
return <p>You have no todo now.</p>;
}

const summary = <p>You have <b>{todos.length}</b> todos:</p>;
const finishLabel = 'Done ✓';

if (platform === 'messenger') {
return (
<>
{summary}
<Messenger.GenericTemplate>
{todos.slice(0, 10).map((todo) => (
<Messenger.GenericItem
title={todo.name}
buttons={
<Messenger.PostbackButton
title={finishLabel}
payload={JSON.stringify({ action: 'finish', id: todo.id })}
/>
}
/>
))}
</Messenger.GenericTemplate>
</>
);
}

return (
<>
{summary}
{todos.map((todo) => <p>{todo.name}</p>)}
</>
);
};

export default TodoList;

The component can then be used in the handleChat like:

src/handlers/handleChat.tsx
import TodoList from '../components/TodoList';
// ...
if (action.type === 'list') {
return reply(
<TodoList
todos={[
{ id: 1, name: 'Buy a mask' },
{ id: 2, name: 'Wear it on' },
{ id: 3, name: 'Be safe' },
]}
/>
);
}
// ...

Now tap the Show Todos 📑 button again, the bot should reply like:

The Done ✓ button post back a 'finish' action with the todo id, we will handle that at the next lesson.

The Component Function

A component is a function with capitalized name. We can use it as the JSX element tag like:

<TodoList todos={[{ id: 1, name: 'foo' }, /* ... */]} />

The first param is the props of the JSX element. TodoList function receives a { todos: [/* ... */] } object. Then we can use the todos to return the UI:

  return (
<>
{summary}
{todos.map((todo) => <p>{todo.name}</p>)}
</>
);

Insert an Array

To display the todos list, we can insert an array of elements in JSX. The {todos.map(todo => <p>{todo.name}</p>)} code above actually shows the same result as:

<>
<p>{todo[1].name}</p>
<p>{todo[2].name}</p>
<p>{todo[3].name}</p>
</>

Cross-Platform Component

To customize messages for the platform, we can return the UI according to platform at the second param. Like:

const TodoList = ({ todos }: TodoListProps, { platform }) => {
// ...
if (platform === 'messenger') {
return (
// messenger UI element
);
}
// ...

At the end of the function, we can return a general UI as the default message:

  // ...
return (
<>
{summary}
{todos.map((todo) => <p>{todo.name}</p>)}
</>
);
};

With this strategy, we can make a component that works on all the platforms.

The Children Prop

Another common strategy is wrapping around the children of the element. Let's use it to make a menu component.

Edit src/components/WithMenu.tsx component like this:

src/components/WithMenu.tsx
//...
type WithMenuProps = {
children: MachinatNode;
todoCount: number;
};

const WithMenu = ({ children, todoCount }: WithMenuProps, { platform }) => {
const info = <>You have <b>{todoCount}</b> todos now.</>;
const listLabel = 'Show Todos 📑';
const listData = JSON.stringify({ action: 'list' });
const addLabel = 'New Todo ➕';
const addData = JSON.stringify({ action: 'add' });

if (platform === 'messenger') {
return (
<>
{children}
<Messenger.ButtonTemplate
buttons={
<>
<Messenger.PostbackButton
title={listLabel}
payload={listData}
/>
<Messenger.PostbackButton
title={addLabel}
payload={addData}
/>
</>
}
>
{info}
</Messenger.ButtonTemplate>
</>
);
}

return (
<>
{children}
<p>{info}</p>
</>
);
};
//...

Then we can use it in handleChat like this:

src/handlers/handleChat.tsx
import WithMenu from '../components/WithMenu';
//...
if (event.type === 'text') {
const matchingAddTodo = event.text.match(/add(\s+todo)?(.*)/i);

if (matchingAddTodo) {
const todoName = matchingAddTodo[2].trim();
return reply(
<WithMenu todoCount={3}>
<p>Todo "<b>{todoName}</b>" is added!</p>
</WithMenu>
);
}
}

return reply(
<WithMenu todoCount={3}>
<p>Hello! I'm a Todo Bot 🤖</p>
</WithMenu>
);
};
//...

Now the menu should be attached like this:

In the codes above, we pass messages to the WithMenu component like:

<WithMenu todoCount={3}>
<p>Hello! I'm a Todo Bot 🤖</p>
</WithMenu>

The <p>Hello! I'm a Todo Bot 🤖</p> is then available as children prop in component function. We can simply return it with the menu attached below. Like:

  return (
<>
{children}
<Messenger.ButtonTemplate
buttons={<>...</>}
>
{info}
</Messenger.ButtonTemplate>
</>
);

You can use this strategy to elegantly decorate the messages, like attaching a greeting, a menu or a feedback survey.


Now we know how to build complicated, cross-platform and reusable chat UI in components. Next, we'll display these UI with real data.