# Cloud Document Button implements data binding solution

When external customer business systems are connected to Feishu Cloud Documents, there is such a demand: to support some **inline text** in cloud documents to associate business system data and realize real-time data update, for example:
> **BU811**'s sales have been good recently, and a launch experiment has been conducted in **Chengdu xxxx 1 Store**.

In this case, the customer hopes that every time the document is opened, **product** and corresponding **store** data can be read from the business system in real time, and business personnel do not need to update the corresponding documents regularly.

Feishu Cloud Document Widget is a great tool for customers to bind cloud documents to business systems. However, there is a problem with using [Text Widget] to bind business systems, that is, the widget design only supports a single line and cannot be embedded in the body as inline text.
Therefore, for such scenarios, Cloud Document provides a text element Button that can bind business data and widgets to achieve this effect. As shown in the figure below:
![20250529162411_rec_.gif](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/d7aba8d6e6430fa0141d341265427386_B8Sq37CZhe.gif?lazyload=true&width=2018&height=1028)

## Implementation plan

### Insert Button

Button is not a Block type, but one of the elements that can be inlined in a text Block.
> For the elements supported by text blocks, please refer to [TextBlockData](https://open.feishu.cn/document/uAjLw4CM/uYjL24iN/docs-add-on/05-api-doc/BlockData/textblockdata/textblockdata), such as: plain text, @User, @Doc, formula...

First, you need to insert a text block, which can be achieved by using [Block.insertBlock](https://open.feishu.cn/document/uAjLw4CM/uYjL24iN/docs-add-on/05-api-doc/basic-data-reference---base/Block.insertBlock) or [Block.appendBlock](https://open.feishu.cn/document/uAjLw4CM/uYjL24iN/docs-add-on/05-api-doc/basic-data-reference---base/Block.appendBlock).

#### Data structure

**Button**
| **Field name** | **Data type** | **Is it required** | **Description** |
| ---------- | ----------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| text | string | Yes | Default text of Button |
| dataSource | object | No | Bind to remote data source |
| ∟url | string | Yes | Data source interface address |
| ∟extra | string | No | Reserved field, meaningless |
| actions | Array | Yes | Actions bound to the button |
| ∟type | string | Yes | Refer to the following ButtonActionType description |
| ∟value | json-string | No | Some Button types require |
**ButtonActionType**
| **value** | **Description** |
| ------------- | ------ |
| OpenDocVerse | Open widget |
| OpenLink | Open hyperlink |
| FollowPage | Follow document updates |
| DuplicatePage | Create a copy |
**ButtonActionValue**
| **Field name** | **Type** | **Action** | **Required** | **Description** |
| ---------- | ------ | ------------ | ------------------------- | ------ |
| blockTypeID | string | OpenDocVerse | Required when action is OpenDocVerse | Widget name |

#### Bind data source

When you insert a Button, specify the `dataSource` field, then you can bind a remote data source. When the Button is loaded, it will initiate a **get** request to `dataSource.url`.

The interface must be returned in the following format:
| **Field name** | **Type** | **Must return** | **Description** |
| -------------- | ------- | -------- | --------------------------------- |
| code | number | Yes | 0 indicates a successful request, other values ​​indicate a failed request |
| data | object | Yes | Return data |
| ∟text | string | Yes | Button text. After a successful request, the existing placeholder text will be replaced with text |
| ∟textStyle | object | No | Text style |
| ∟color | string | No | Text color |
| ∟background | string | No | Text background |
| ∟bold | boolean | No | Whether bold |
| ∟italic | boolean | No | Whether italic |
| ∟underline | boolean | No | Whether to add underline |
| ∟strikethrough | boolean | no | whether to add a strikethrough |

#### Code example

```js
import { BlockitClient } from 'doc-mini-app-client';
import { BlockType } from 'node_modules/@bdeefe/doc-mini-app-client/dist/types';
const docMiniApp = new BlockitClient().initAPI();
const docRef = await docMiniApp.getActiveDocumentRef();
const rootBlock = await docMiniApp.Document.getRootBlock(docRef);
const data = await docMiniApp.Block.appendBlock(rootBlock.ref, {
  type: BlockType.TEXT,
  data: {
    plain_text: 'Placeholder text',
    text: {
      elements: [{
        button: {
          text: 'Placeholder text',
          dataSource: {
            // Business backend interface address
            url: 'https://bytedance.larkoffice.com/api/datasource',
            extra: ''
          }
        }
      }]
    }
  }
});
```

The data returned by the /api/datasource interface in the example is as follows:

```js
// GET: https://bytedance.larkoffice.com/api/datasource
// Response:
{
  code: 0//Status code 0=Request successful
  data: {
    text: 'Chengdu Kuanzhai Alley 1 Store' //Return text
    textStyle: {
      color: '#f00', //Text color
      background: '#fff', //Text background
      bold: false, //Bold
      italic: false, //Italic
      underline: false, //Underline
      strikethrough: true //Strikethrough
    }
  }
}
```

### Button Binding Widget

After implementing Button data binding, the customer made another request:
> I hope that when I `hover` or `click` a Button, a panel will pop up to show detailed information.

To achieve this effect, you can use an Action of Button: `OpenDocVerse`. It allows Button to bind a specified cloud document widget, and when the user interacts with the Button, the widget will pop up, thus meeting customer needs.
When calling a widget, if you want to pass relevant parameters to the widget, you can add the parameters to the **value** field of actions. In the widget, use the API `DocMiniApp.Events.onInlineComponentTriggerChange` to add a listening function. When the Button hover event is triggered, the listening function can receive the value.

#### Code Example

```js
import { BlockitClient } from 'doc-mini-app-client';
import { BlockType, ButtonActionType } from 'node_modules/@bdeefe/doc-mini-app-client/dist/types';
const docMiniApp = new BlockitClient().initAPI();
const docRef = await docMiniApp.getActiveDocumentRef();
const rootBlock = await docMiniApp.Document.getRootBlock(docRef);
const data = await docMiniApp.Block.appendBlock(rootBlock.ref, { 
  type: BlockType.TEXT, 
  data: { 
    plain_text: 'placeholder text', 
    text: { 
      elements: [{ 
        button: { 
          text: 'placeholder text', 
          backgroundColor: 'rgb(225,234,255)', dataSource: {
            // Business backend interface address
            url: 'https://bytedance.larkoffice.com/api/datasource',
            extra: ''
          },
          // Add the following code
          actions : [{
            // Open document widget
            type : ButtonActionType . OpenDocVerse ,
            value : JSON . stringify ({
              // Bind the corresponding widget
              blockTypeID : 'blk_xasdf' ,
            }),
          }]
        },
        style: {}
      }]
    }}
});
```

### Control the visibility of widgets

```js

# Cloud document widget listens to Button hover events

class Component {
  useEffect(() => {
    DocMiniApp.Events.onInlineComponentTriggerChange(
      (event: InlineComponentTriggerChangeEvent) => {
        const { type, value } = event;
        if (type === 'enter') {
          // When the mouse hovers, display the widget
          DocMiniApp.Bridge.updateVisiable(true);
        } else if (type === 'leave') {
          DocMiniApp.Bridge.updateVisiable(false);
        }
      }
    );
  }, []);
}
interface InlineComponentTriggerChange {
    type: InlineComponentTriggerType;
    rect: {
        x: number;
        y: number;
        top: number;
        bottom: number;
        left: number;
        right: number;
        width: number;
        height: number;
    };
    windowSize: {
        width: number;
        height: number;
    };
    value: any;
}

enum InlineComponentTriggerType {
    Enter = "enter",
    Leave = "leave",
    Click = "click",
    // 暂未支持
    Dblclick = "dblclick",
    // 暂未支持
    Longpress = "longpress"
}
```

## Q & A

### Q: What should I do if the widget is reloaded every time?

**A**: You can configure the widget to be kept alive in the background. Find app.json and add the `isKeepAlive` configuration.
```js
{
  ...
  "appID": "cli_xxxxx",
  "blockTypeID": "blk_xxxxxx",
  "contributes": {
    "topbar": {
      ...
      "view": "index.html",
      "hideHeader": true,
      "isKeepAlive": true
    }
  }
}
```

### Q: How to insert a Button at the cursor position?

**A:** The cursor can be placed at the beginning, end, or even in the middle of a line, and there are many scenarios. Especially when it falls in the middle of a line, the Button will split the original text into two parts. How to handle these scenarios requires more code logic. You can refer to the following demo:
```js
async function addButton() {
  const docRef = await DocMiniApp.getActiveDocumentRef();
  const selection = await DocMiniApp.Selection.getSelection(docRef);
  if (selection?.[0]?.type !== 'text') {
    return;
  }
  const button = {
    button: {
      text: 'BA Button',
      backgroundColor: 'rgb(225,234,255)',
      dataSource: {
        url: `https://bytedance.feishu.cn/api/test?uniqueCode=BA000-1807001051995635713`,
        extra: '',
      },
      actions: [{
        type: 'OpenDocVerse',
        value: JSON.stringify({
          blockTypeID: 'xxxxx',
          id: 'BA000-1807001051995635713'
        })
      }]
    },
    style: {}
  };

const { range } = selection[0].ref as any;
  const selectedBlocks = await DocMiniApp.Selection.getSelectedBlocks(docRef);
  const currentBlock = await DocMiniApp.Block.getBlock(selectedBlocks[0].ref);
  const { elements } = currentBlock.data.text;
  let totalIndex = 0;
  const newElements = [];
  let inserted = false;
  for (const item of elements) {
    if (!inserted && item.text_run) {
      const contentLength = item.text_run.content.length;
      const start = totalIndex;
      const end = totalIndex + contentLength;
      if (start <= range[0] && end >= range[0]) {

const splitPos = range[0] - start;
        newElements.push({
          text_run: {
            ...item.text_run,
            content: item.text_run.content.slice(0, splitPos)
          }
        });

newElements.push(button); 
        newElements.push({
          text_run: {
            ...item.text_run,
            content: item.text_run.content.slice(splitPos)
          }
        });
        inserted = true;
        totalIndex += splitPos + 1;
      } else {
        newElements.push(item);
        totalIndex += contentLength;
      }
    } else if (!inserted && totalIndex === range[0]) {

newElements.push(button);
      newElements.push(item);
      inserted = true;
      totalIndex += 2;
    } else {
      newElements.push(item);
      totalIndex += item.text_run ? item.text_run.content.length : 1;
    }
  }

if (!inserted) {
    newElements.push(button);
  }
  await DocMiniApp.Block.updateBlock(currentBlock.ref, {
    text: {
      elements: newElements,
    }
  });
}
```

### Q: How to pass parameters to a widget when invoking it?

**A:** When invoking a widget, if you want to pass relevant parameters to the widget, you can add the parameters to the **value** field of button.actions. The callback function that listens to the `DocMiniApp.Events.onInlineComponentTriggerChange` event inside the widget will receive the value.