Stimulus + TypeScript + Slim で「もっと見る」「閉じる」を作る

要件

・viewはslim ・可変なliが渡ってくるsidemenuでもっとみる/とじるをぬるっと動かす

とりあえずviewはこんな感じ

- unique = SecureRandom.hex(8)
.show-more[data-controller="show-more"]
  input.show-more__check[type="checkbox" id="show-more__check-#{unique}"]
  .show-more__content[data-target="show-more.content"]
    .show-more__conetnt-wrapper
      = yield
  label.show-more__label[for="show-more__check-#{unique}" data-target="show-more.label" data-action="click->show-more#toggle"]
  • 複数箇所で使われることを想定して、特定のcheckboxに対するlabelであることを担保するためにidをuniqueにしました

  • すべてを囲う show-more に対して data-controller="show-more" をつけることで、stimulus の show_more_controller.ts にアクセスできます。もちろん名前はなんでもOK。

  • labelの「もっとみる」「とじる」をcssで変更させたかったので、inputでopen/closeをもつようにしました。それとあわせてjs側でもそれを担保するために、controller内で state 的な値を持たせています。

  • これを使う側で中身を可変にできるために、yieldを挟んでいます。一度限りしか使わないパーツならyieldは不要で、その部分にlist的なものを入れてあげればOKです。

続いてStimulus側

import { Controller } from 'stimulus'

export default class ShowMoreController extends Controller {
  static targets = ['content', 'label']

  contentTarget!: HTMLElement

  private isShow = false

  private static readonly DEFAULT_CONTENT_HEIGHT: number = 220

  toggle(): void {
    if (this.isShow) {
      this.updateShowStatus(false)
      this.contentTarget.style.height = `${ShowMoreController.DEFAULT_CONTENT_HEIGHT}px`
    } else {
      this.updateShowStatus(true)
      this.contentTarget.style.height = `${this.contentHeight}px`
    }
  }

  private updateShowStatus(flag: boolean): void {
    this.isShow = flag
  }

  private get contentHeight(): number {
    if (!this.contentTarget || !this.contentTarget.firstElementChild) {
      return ShowMoreController.DEFAULT_CONTENT_HEIGHT
    }

    return this.contentTarget.firstElementChild.clientHeight
  }
}
  • 一度labelをクリックしたら、toggle() が呼ばれます。基本的にもっとみるは最初閉じられているので、デフォルトでfalseが設定されている isShow をみて、開くのか閉じるのかを決定させます。

  • ここが大事!今回は可変な要素に対してtransition(アニメーション) をつけたかったのですが、height: autoに対して基本的にtransitionをつけることができませんでした。なので、jsで要素のheightを変更させるようにした感じです。もちろんここをjqueryで書いてもいいのですが、jqueryはちょっと...という場合にこの方法を採りました。

  • 定数として定義している DEFAULT_CONTENT_HEIGHT は最初にちょっと出す部分のheightである220を設定していますが、そこは要件によりよしなに変更していただければ。

最後にscss

.show-more {
  margin-bottom: $space-XXL;
  .show-more__content {
    position: relative;
    overflow: hidden;
    height: 220px;

    transition: all 400ms ease;
  }

  .show-more__content::before {
    display: block;
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    content: "";
    height: 50px;
    background: $co-white;
    opacity: .6;
  }

  .show-more__label {
    margin: 0 auto;
    position: absolute;
    left: 50%;
    width: 100%;
    height: 44px;
    line-height: 44px;
    bottom: -20px;
    transform: translateX(-50%);
    background-color: $co-light-gray;
    border-radius: 0 0 $round $round;
    color: $co-dark-gray;
    font-weight: bold;
    cursor: pointer;
    text-align: center;
  }

  .show-more__label::before {
    content: '続きを読む';
    margin-right: $space-S;
  }

  // FIXME font awesome的なのを入れて自作を回避したい
  .show-more__label::after {
    font-size: 4px;
    content: '▼';
  }

  .show-more__check {
    display: none;
  }

  .show-more__check:checked ~ .show-more__label {
    bottom: 0px;
  }

  .show-more__check:checked ~ .show-more__label::before {
    content: '閉じる';
    margin-right: $space-S;
  }

  .show-more__check:checked ~ .show-more__label::after {
    font-size: 4px;
    content: '▲';
  }

  .show-more__check:checked ~ .show-more__content::before {
    display: none;
  }
}
  • contentで文字を定義していますが、ここはjsやslim側で定義してあげてもよかったかなと。I18n使ってたりすると辛いです。

参考

Stimulusの詳しい使い方はまずはリファレンス: stimulusjs.org

カンタンに文法をみるならこの記事がよかった: qiita.com