Figmaに表を作り出すプラグインの作成

# この記事の概要

Googleスプレッドシートからデータを取得しFigmaに表を出力するプラグインを作成した
プラグインの内容を元に、Figmaの関数やnodeプロパティについての情報をまとめた
行いたい処理、それに必要な実際のコードとを記載している

# 前提

クロスドメインのためFigmaから直接GoogleAPIを呼び出すことはできない  
そのためAPI GatewayからAPIを呼び出せるようにしている

https://qiita.com/shiho_hoshino/items/1265db07667cdcb5c770

Figmaプラグインを開発できる環境がある

https://qiita.com/shiho_hoshino/items/3718a28e4d302d5d0ea6

# 作成したプラグインの概要

作成したプラグインのコード

https://github.com/shiho-hoshino/figma_table_creator

1. 行のレイアウトになる要素をFigmaで作成する  
   ※ 列のテキスト要素のレイヤー名を`#col`としておく
   ※ 列の要素数はスプレッドシートの数と同じにする
1. ブラグインを起動して作成した要素を選択
1. スプレッドシートのIDを入力して`サービスアカウントからデータ取得`ボタンをクリック
1. 選択していた要素の下に表が出力される

![figma1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/f7bb4136-0c59-6c4a-a8dd-21dd85d90a05.png)

### プラグインで行っていること

- FigmaからAPIでデータを取得する
- 指定の名前のnodeを取得する
- nodeをcloneする
- nodeにテキストを追加する

# コード解説

1. ui.htmlでAPIを呼び出す
1. ui.htmlからAPIで取得したデータをcode.tsで受け取る
1. Figmaで選択中の要素を取得する
1. 描画するnodeを準備する
1. 準備したnodeを出力する

### ui.html / code.ts コード

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="abqoppV" data-user="shiho-hoshino" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/shiho-hoshino/pen/abqoppV">
  Figma_Table_Creator</a> by shiho (<a href="https://codepen.io/shiho-hoshino">@shiho-hoshino</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## 1.ui.htmlでAPIを呼び出す

- 用意するもの
  - API URI（AWS API Gateway） 
  - APIキー（AWS API Gateway） 

以下ui.htmlのコード抜粋
クリックされたらXMLHttpRequestでスプレッドシートのデータを取得  
取得できたらpostMessageでデータをcode.tsに送信する

```
<script>
const loader = document.getElementById('loader');
document.getElementById('getLocalApi').onclick = () => {
  loader.classList.add('fadeIn');
  const sheetId = document.getElementById('sheetId').value;
  const range = document.getElementById('range').value || 'シート1';

  
  const request = new XMLHttpRequest()
  request.open('GET', `[API_URL]/${sheetId}/${range}`)
  request.setRequestHeader('Content-Type', 'application/json');
  request.setRequestHeader('x-api-key', '[APIキー]');
  request.responseType = 'json'
  request.onerror = function(e){
    console.log(e);
    alert('データが取得できませんでした');
    loader.classList.remove('fadeIn');
  };
  request.onload = () => {
    if (request.status === 200) {
      parent.postMessage({ pluginMessage: {
        type: 'setData',
        response: request.response,
      }}, '*')
    }
    loader.classList.remove('fadeIn');
  };

  request.send()
}
</script>
```

## 2.ui.htmlからAPIで取得したデータをcode.tsで受け取る

- `figma.showUI` モーダルのダイアログとしてHTMLを呼び出せる
  - 参考： https://www.figma.com/plugin-docs/api/properties/figma-showui/#docsNav
- `__html__` manifest.json ファイルの "ui" フィールドにファイル名を割り当てた場合、そのファイルの内容を取得できる
  - 参考： https://www.figma.com/plugin-docs/api/api-reference/#html
- `figma.ui.onmessage`： uiフィールドのpostMessageを検知できる
  - 前述のui.htmlでMessageには`.type`と`.response`オブジェクトを追加している
    - `.type` どのボタンのpostMessageか
    - `.response` APIの返り値

```typescript
figma.showUI(__html__);
figma.ui.onmessage = async msg => {
  if (msg.type === 'setData') {
    /**
     * Googleスプレッドシートから取得する情報 response.data.values
     */
    const values = JSON.parse(JSON.stringify(msg.response.data.values));
```

## 3.Figmaで選択中の要素を取得する

- `currentPage` 現在みているページのnodeを取得
- `currentPage.selection` 選択中のnodeを取得
  - ここで選択されているのは`テーブルの行`のパーツになる要素1件である想定

```typescript
let selection: any = [];
const {currentPage} = figma;
const selectionNode = currentPage.selection;

if (selectionNode.length > 0) {
```

## 4.描画するnodeを準備する

選択している行要素のクローンして配列として`cloneNodes`変数に追加していく

- `figma.loadFontAsync`  フォント情報を読み込む
  - これを先にやらないと要素にテキスト追加が行えないので完了をthenで検知しておく
- `SceneNode` FigmaのNode型定義
  - 参考 https://www.figma.com/plugin-docs/api/nodes/
- `getColNodes` テキストを追加する列のnodeを取得する独自関数、詳細は後述
- `changeText` getColNodesで取得した要素のテキストを差し替える独自関数、詳細は後述

```typescript
const cloneNodes: SceneNode[] = [];
figma.loadFontAsync({
  "family": "Helvetica",
  "style": "Regular"
}).then(() => {
  selectionNode.map((node, index) => {
    // スプレッドシートの行ごとに処理
    values.map((value, i) => {
      // nodeをcloneし配列に追加
      cloneNodes.push(node.clone())
      // cloneしたnodeからテキストを反映するnodeを取得
      const colNodes = getColNodes(cloneNodes[i]);
      // 指定要素にテキストを反映
      colNodes.map((node, i) => {
        const text = value[i] || '';
        changeText(node, text);
      });
    });
  })
```

**setText**

- children: 子要素が含まれるとchildrenオブジェクトが存在する
  - 子要素も含め対象のnodeの中から「#cal」のnameを持つ要素を取得する

```typescript
/**
 * @param obj 行
 * @param nodes 最終的なreturn値
 */
const getColNodes = function (obj, nodes = []) {
  if (Array.isArray(obj)) {
    obj.map((node: any, index) => {
      if (node.name.indexOf("#col") != -1) {
        nodes.push(node);
      } else if (node.children) {
        getColNodes(node.children, nodes);
      }
    });
  } else {
    if (obj.name.indexOf("#col") != -1) {
      nodes.push(obj);
    } else if (obj.children) {
      getColNodes(obj.children, nodes);
    }
  }

  return nodes;
};
```

**setText**

- .characters: nodeがテキストの場合charactersオブジェクトが存在する
  - 参考 https://www.figma.com/plugin-docs/api/TextNode/#text-content
- deleteCharacters： nodeのテキストを削除する
  - 参考： https://www.figma.com/plugin-docs/api/properties/TextNode-deletecharacters/#docsNav
- insertCharacters： nodeに指定場所からテキストを追加する
  - 参考： https://www.figma.com/plugin-docs/api/properties/TextNode-insertcharacters/#docsNav

```typescript
/**
 * @param node テキストを反映するNode
 * @param text 変更するテキスト
 */
const changeText = function (node, text) {
  const obj: any = {};
  if (node.characters) {
    try {
      node.deleteCharacters(0, node.characters.length);
      node.insertCharacters(0, text);
    } catch (e) {
      console.log(e);
    }
  }
};

```

## 5.準備したnodeを出力する

- `createFrame` フレームを作成する
- `.x` 選択中の要素のx軸の数値を取得
- `.y` 選択中の要素のy軸の数値を取得
- `.height` 選択中の要素の高さを取得
- `insertChild` nodeに追加（フレームとブロックnodeにのみ可能）
- `layoutMode` VERTICALで、Y軸基準にする
- `primaryAxisSizingMode`　layoutModeで設定した軸のサイズのモードを変更する  
  サイズの固定か自動を選べる、autoで中のテキストサイズに合わせてY軸（VERTICAL）可変になる
- `counterAxisSizingMode` layoutModeで設定した軸とは逆の軸のサイズモードを変更する

```typescript
// フレーム作成
const frame = figma.createFrame();

figma.currentPage.selection = [frame];

// フレームの位置を設定
const x = selectionNode[0].x;
const y = selectionNode[0].y + selectionNode[0].height + 20;
frame.x = x;
frame.y = y;

// Auto layout を有効にする
frame.layoutMode = "VERTICAL";

// フレームに作成した行Nodeを追加
cloneNodes.map((node, index) => {
    frame.insertChild(index, node);
});

// コンテンツサイズで自動リサイズするモードに変更
frame.primaryAxisSizingMode = "AUTO";
frame.counterAxisSizingMode = "AUTO";
```

以上

# まとめ

Figmaのプラグイン開発をスムーズに行うにはNode構造の把握が不可欠だった

**注意点**
Figmaは Figma design と FigJam の二種類のツールがあり
Figmaのドキュメントには二つのツールのNodeプロパティについての解説が混在している

Figma designで使えると思った関数がFigJamのもので使えないことなどもあったので
どちらのものなのか都度確認した方が良い