✏️

ReactとCSSで隣接要素の状態を検知し、動的にスタイルを変更しよう

2024/07/13に公開

はじめに

最近業務で ⬇️ こんな感じのデザインを実装することになりました。

閲覧モードと編集モード(フォーム)を切り替えることができ、編集モードの時は上・下・もしくは上下に境界線を表示させて間隔も広げるといった感じです。

最初に見た時は、一つ前の要素の状態を知らないといけないから親コンポーネント側で状態管理しないとダメかなーとかぼんやり思っていたんですが、やってみると意外とシンプルに実装する方法があり、学びがあったので記事にすることにしました。

この記事を読んでほしい人

  • React を使って、隣接する要素の状態(今回は編集モード or 閲覧モード)に基づいてスタイルを動的に変更したい人

使用技術

  • React
  • TypeScript
  • Emotion

ざっくり要件

  1. 各要素は閲覧モードと編集モードを切り替えられる
  2. 編集モードの時は境界線を表示する(間隔も広げる)
  3. それぞれの要素は独立して更新できる
  4. 要素は更新されると順番が入れ替わることがある
  5. 自分が編集状態の時、自分以外の要素が更新されても編集状態はキープする

冒頭で貼ったイメージをみてもらえれば大体イメージがつくかと思いますが、ざっくりこんな感じの要件でした。

困りポイント

まず先に、実装するにあたっての困りポイントを書いてみます。

1. 編集モードの時に要素間の間隔が変わる

1つ目の困りポイントですが、何も考えずに一旦上下線と間隔を広げることだけを考えると、下記のような感じになるかと思います。

※このコンポーネントが複数並ぶと思ってください。

const [isEdit, setIsEdit] = useState(false);

<li css={[props.isEdit && editModeStyle]}>
  { isEdit ? <EditMode /> : <ViewMode onEdit={() => setIsEdit(true)}/> }
</li>

const editModeStyle = css`
  :not(:last-child) {
    padding-bottom: 32px;
    border-bottom: 1px solid gray;
  }
    
  :not(:first-child) {
    padding-top: 32px;
    border-top: 1px solid gray;
  }
`;

お分かりの方も多いかと思いますが、これだと ⬇️ のような問題が起きます。

2. 更新されると要素の順番が入れ替わることがある + 自分以外の要素が更新されても編集状態はキープする

2つ目の困りポイントです。
これがちと厄介で、更新時に index がずれる可能性があるため、親側で全ての要素の状態を管理する方法は取れなくなりました。
※ちなみに、各要素はそれぞれを一意にするためのフィールド(ID とか)を持っていません。

⬇️ 厄介なやーつ

うーん。どうしたもんかと悩んでいたら、一発でこの問題を解決してくれる救世主と出会いました。

data 属性を使ってサクッと解決

MDN のドキュメントによると data 属性とは、

data-* 属性により、標準外の属性や DOM の追加プロパティなどの特殊な方法に頼らずに、標準的な意味のある HTML 要素に追加情報を格納することができます。

と言うことらしく、要は独自にタグ付けができる機能って感じですかね。
JavaScript や CSS から読み出すのも簡単みたい。

今回のケースでも、下記の 2 ステップで解決できました。

  1. data 属性で編集状態を管理する state を渡す
  2. 一つ前の要素が編集中かどうかを data 属性から把握し、スタイルを適用する

1.data 属性で編集状態を管理する state を渡す

const [isEdit, setIsEdit] = useState(false);

- <li css={[props.isEdit && editModeStyle]}>
+ <li css={[props.isEdit && editModeStyle]} data-is-edit={isEdit}>
    { isEdit ? <EditMode /> : <ViewMode /> }
  </li>

これだけです。簡単ですね。

2.一つ前の要素が編集中かどうかを data 属性から把握し、スタイルを適用する

const editModeStyle = css`
  ${mq.mobile} {
    :not(:last-child) {
      border-bottom: 1px solid ${colors.gray100};
      padding-bottom: 32px;
    }

    :not(:first-child) {
      padding-top: 32px;
      border-top: 1px solid ${colors.gray100};

+     :where([data-is-edit="true"] + [data-is-edit="true"]) {
+       border-top: none;
+       padding-top: 0;
+     }
    }
  }
`;

大事なのは :where([data-is-edit="true"] + [data-is-edit="true"]) { の部分です。
+ は、その要素の後ろ隣にある要素をセレクタにします。
:where は引数で渡されたセレクタに当てはまる要素を探します。

つまり、「data-is-edit が true の要素の後ろ隣にある data-is-edit が true の要素」を探してスタイルをあてるわけですね。
要は「一つ前が編集中で、自分も編集中」というです。

一つ前が編集中の時は、すでに十分な感覚が空いており境界線も引かれているため、border も padding も設定しないようにします。。

これで冒頭で示したデザインを実現することができました。あっさり実装できてしまいましたね。
data 属性バンザイです。

まとめ

今回の記事を簡単にまとめます。
動的に状態が変更され得る複数の要素を並べ、一つ前の要素の状態に応じて動的にスタイルをあてたいときには、

1. data 属性でその状態を管理する
2. :where() 等の擬似クラス関数で要素を特定してスタイルをあてる

で実現することができます。

終わりに

今回は、data 属性を使って一つ前の要素の状態を把握し、動的にスタイルを変更する方法を紹介させていただきました。

とってもシンプル!
data 属性便利ですね。
今回のようなデザインの実装パターンを覚えておけば、意外と色々なところで使えそうです。

こちらの記事がどなたかの役に立てば幸いです。それではまた✋

Discussion