フリーランスWebエンジニア HTNCode

▲
  • HOME
  • ABOUT
  • SERVICE
  • ACHIEVEMENT
  • BLOG
  • CONTACT
  • githubIcon
  • twitterIcon
プライバシーポリシー
footerLogoImage

© HTNCode All Rights Reserved.

eyecatch

コードブロックにコピーボタンを追加する

作成日時:2024年7月1日 00:01

更新日時:2024年7月1日 00:02

Next.js

React

microCMS

はじめに

(これは2023年11月に投稿した記事の再投稿です。諸事情により記事閲覧ができなくなっていたため修正しました)

 
さて今回は、Blogページの各記事において、コードブロックにコピーボタンほしいよな~と思いながらも未着手だったので、実装してみた話をしようと思います。

Next.jsでクライアントコンポーネントとサーバーコンポーネントとを共存させる方法に悩まれている方などのご参考になれば幸いです。
※当サイトはNext.js 14で制作しています。

 

  

構成パターン

まず話に入っていく前に、前提から。
Next.jsでは、クライアントコンポーネントとサーバーコンポーネントとを共存させるには、明確にコンポーネントを分ける必要があります。
その辺の構成パターンは公式サイトで解説されていますので、前知識としてご参考ください。

というかこれ見れば一発なんですが(笑)

 

■Server and Client Composition Patterns | Next.js
https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns
 

 

さっそくコンポーネントを分ける

今回、microCMSからブログ情報を取得し、それを以下のように展開している部分のうち、コードブロック、
つまり<pre><code></code><pre>内にコピーボタンを差し込めれば良いわけです。

 

しかし、postは同一のコンポーネント内でmicroCMSからブログ情報を取得し、その取得した情報をあーだこーだする処理含めて、
結構サーバー側の記述が多くなってたので、以下部分のみをクライアントサイドのコンポーネントとして切り出した方が早いと判断。

 

          <div
            className={styles.content}
            dangerouslySetInnerHTML={{
              __html: `${post.content}`,
            }}
          />

 

以下のように切り出しました。
切り出したコンポーネント側でもブログ情報(具体的に言うと本文の情報)が必要なので、propsで渡しています。

 

          <ClientComponent post={post} />

 

 

クライアントコンポーネントの内容

実装方法は色々あると思うのですが、useRefとuseEffectを使ってDOM要素を取得し、ボタン要素を追加するという処理にしました。
ボタンを押したときに、コードブロック部分がコピーされてほしいので、コピーの処理も入れます。
(型定義で「post: any」となっている部分はよくないのですが、とりあえずということで後回し)

 

// サーバーコンポーネントと分けるために切り出し
"use client";
import React, { useEffect, useRef } from "react";
import styles from "@/app/styles/CopyBtn.module.scss";

interface Props {
  post: any;
}

const ClientComponent = ({ post }: Props) => {
  const contentRef = useRef<HTMLDivElement>(null);

  //コードブロック部分に対してコピーボタンを追加
  useEffect(() => {
    if (contentRef.current) {
      const codeBlocks = contentRef.current.querySelectorAll("pre code");
      codeBlocks.forEach((block: Element) => {
        const htmlBlock = block as HTMLElement;
        const button = document.createElement("button");
        button.textContent = "copy";

     // コピーボタンクリック時の処理
        button.onclick = function () {
          navigator.clipboard.writeText(htmlBlock.innerText);
        };

        if (htmlBlock.parentNode) {
          htmlBlock.parentNode.insertBefore(button, htmlBlock);
        }
      });
    }
  }, []);

  return (
    <>
      <div
        className={styles.content}
        dangerouslySetInnerHTML={{
          __html: `${post.content}`,
        }}
        ref={contentRef} // Refを適用しDOM操作できるようにする
      />
    </>
  );
};

export default ClientComponent;

 

これをサーバーコンポーネント側にimportして使えばOKです。エラーも出ません。

あとは、CSSで煮るなり焼くなりしてボタンのデザインをいじります。

 

 

終わりに

いかがでしたか??そもそもは、こういうことが後で起きると面倒なので、
最初からきちんとクライアントコンポーネントとサーバーコンポーネントを意識、分けて作っておくと後で楽ですね。

それではまた。

 

HTNCode