Stencil.jsを使って、インタラクティブなGROWIプラグインを作る

[オープンソースのWikiであるGROWI](https://growi.org/ja/)にはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。

今回は、インタラクティブな操作が可能になるGROWIプラグインを作ってみました。テンプレートも用意したので、手軽にGROWIプラグイン開発をはじめられます。

## GROWIプラグインの難点

これまで、幾つかのGROWIプラグインを開発してきましたが、若干の不便さがありました。それは、Reactコンポーネントで表示処理を実装できるものの、useStateやasync/awaitがそのままでは実装できないことです。

ネットワークでデータを取得して表示に利用するというのは、 `react-async` を使えば解決できますが、あらかじめデータを取得する必要があるなど若干の不便さは残ります。

特に、ボタンを押したタイミングで何かを実行したり、ユーザーの入力を受け付けるようなGROWIプラグインが実現できないのは不便さがありました。

## Stencil.jsを使う！

GROWI自体はReactを作られており、全体がReactアプリとしてマウントされています。その中で別なReactコンポーネントを作り、インタラクティブな機能を実現できません。

そこで、解決策として考えたのが[Stencil.js](https://stenciljs.com/)です。Stencil.jsは、Web Componentsを作るためのフレームワークで、できあがったWeb ComponentsはReactやVue.js、Angularなどでも利用できます。そして、Stencil.jsのWeb Componentsの中では、独自にインタラクティブなUIであったり、画面への表示反映などが実現できます。

ということで、Stencil.jsをGROWIプラグインの中で使ってみました。

## できたもの

これはデモですが、GROWIでボタンを表示しています。そして、そのボタンを押すとカウントアップします。ネットワーク処理ももちろん使えるので、データを保存したり、逆に呼び出すことも可能です。

![FireShot Capture 375 - Stencil - GROWI - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/afe33645-166c-14d8-c808-b0536507e1de.jpeg)


## 作り方

テンプレートは[goofmint/growi\-plugin\-script\-template\-stencil: GROWI script plugin template with stencil](https://github.com/goofmint/growi-plugin-script-template-stencil)に作成しています。このプラグインでは `<growi-component />` というタグを定義しています。

手順としては、まずStencil.jsでWeb Componentsを作成します。

### Web Componentsの作成

`growi-component/src` 以下を編集します。 `growi-component/src/components/my-component/growi-component.tsx` を見ると、だいたい内容が分かるかと思います。

`render` メソッドでレンダリングを行い、変更される変数は `@State` で指定します。イベントは `onClick` などを使います。

```tsx
import {
  Component, Prop, State, h,
} from '@stencil/core';

// import { format } from '../../utils/utils';

@Component({
  tag: 'growi-component',
  styleUrl: 'growi-component.css',
  shadow: true,
})
export class GrowiComponent {

  /**
   * The name
   */
  @Prop() name: string;

  /**
   * The parametar1
   */
  @Prop() params1: string;

  /**
   * The parametar2
   */
  @Prop() params2: string;

  /**
   * The count
   */
  @State() count = 0;

  private getText = (): string => {
    return `${this.name}. params1: ${this.params1} params2: ${this.params2}`;
  };

  onClick = (): void => {
    this.count += 1;
  };

  render(): JSX.Element {
    return (<div>Hello, World! I'm {this.getText()}! Count: {this.count}
      <button onClick={this.onClick}>Click me</button>
    </div>);
  }
}
```

### Web Componentsのビルド

ビルドは `pnpm run build` でOKです。開発中は `pnpm run build --watch` しておくと便利です。

### Web Componentsの読み込み

GROWIでは、 `src/Hello.tsx` にてWeb Componentsを読み込んでいます。

```tsx
import '../growi-component/dist/components/growi-component';
```

読み込んでしまえば、後はレンダリングする際に `<growi-component />` というタグを使うだけです。

```tsx
export const helloGROWI = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
  return ({ children, ...props }) => {
    try {
      const { stencil, params1, params2 } = JSON.parse(props.title || '{}');
      if (stencil) {
        return (
          <growi-component
            name={children}
            params-1={params1}
            params-2={params2}>
          </growi-component>
        );
      }
    }
    catch (err) {
      // console.error(err);
    }
    // Return the original component if an error occurs
    return (
      <Tag {...props}>{children}</Tag>
    );
  };
};
```

GROWIページ内では、 `:plugin[label]{params1=test2 params2=test2}` のように指定します。これをRemark Directiveとして処理する際に、 `a` タグに置き換えています。一緒に `stencil = true` も追加しておくことで、表示する際にプラグインなのかどうかを判別できます（上のコードへの処理分けに使っています）。

```tsx
export const remarkPlugin: Plugin = () => {
  return (tree: Node) => {
    visit(tree, (node: Node) => {
      const n = node as unknown as GrowiNode;
      if (n.name !== 'plugin') return;
      const data = n.data || (n.data = {});
      // Render your component
      const { value } = n.children[0];
      data.hName = 'a'; // Tag name
      data.hChildren = [{ type: 'text', value }]; // Children
      // Set properties
      data.hProperties = {
        href: 'https://example.com/',
        title: JSON.stringify({ ...n.attributes, ...{ stencil: true } }), // Pass to attributes to the component
      };
    });
  };
};
```

## Stencil.js + GROWIプラグインの可能性について

Stencil.jsを使うことで、以下のようなことが可能になると考えています。

- CSSにTailwindを使う
- RSSを取り込んで表示する際に、ついでにページを作る。これでRSSの内容をGROWIの検索対象にする
- モーダルを表示して、データをメンテナンスする
- 独自のBaaS（Supabaseなど）と組み合わせて、掲示板などを実装する
- Backlog APIなどからデータを取得するだけでなく、データメンテナンスも行う

実体はWeb Componentsなので、GROWIに限らず利用できますが、GROWIの中に組み込んで使えるのが利点ではないでしょうか。

## GROWIコミュニティについて

プラグインの使い方や要望などがあれば、ぜひGROWIコミュニティにお寄せください。実現できそうなものがあれば、なるべく対応します。他にもヘルプチャンネルなどもありますので、ぜひ参加してください！

[GROWI Slackへの参加はこちらから](https://communityinviter.com/apps/wsgrowi/invite)

## まとめ

GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。

[OSS開発wikiツールのGROWI | 快適な情報共有を、全ての人へ](https://growi.org/ja/)