WebDesign Dackel

React.jsでリストの追加・削除時にアニメーションを付ける

React.jsでリストの追加・削除時にアニメーションを付ける

Hatena0
Google+0
Pocket0
Feedly0

はじめに

この前はReact.jsをCoffeeScriptで書いていくための環境を構築しました。
少し時間があいてしまいましたが、やっと公式のチュートリアルが終わりました。

チュートリアルは、簡単にいうと

  • コメントの一覧を表示
  • コメントの追加
  • コメントの削除

が出来るようなものだったのですが、どうせならもう少しリッチに見せたいです…!

というわけで今回はチュートリアルに似たような構成で、各リストの追加・削除のタイミングでアニメーションを付けるサンプルを作成してみました。

動作サンプル

動作サンプル

実際に動作しているサンプルはこちら

ページの上のほうにあるフォームから、タイトルと内容を入力して「Add!!」ボタンをクリックで新しいリストが追加されて、タイトルの横にあるバツ印をクリックすると削除されるという単純なサンプルになっています。

いざ実装

基本的な構造は、前回構築した構成を使いました。

tsuyoshiwada/sample-react-coffee

コード全体

重要そうなポイントは後で書くとして、まずは全体のコードから書いておきます。
各コンポーネントは下図の様な構成になっています。

コンポーネントの構成図

React = require("react/addons")
CSSTransitionGroup = React.addons.CSSTransitionGroup
LinkedStateMixin = React.addons.LinkedStateMixin


# カードの操作
CardControl = React.createClass(
  mixins: [LinkedStateMixin]

  propTypes:
    onSubmit: React.PropTypes.func.isRequired

  getInitialState: ->
    title: ""
    contents: ""

  handleSubmit: (e) ->
    e.preventDefault()
    title = @state.title.trim()
    contents = @state.contents.trim()

    if title != "" && contents != ""
      # <textarea>の値は改行させる
      # 参考: http://niwaript.niwaringo.com/entry/2015/04/07/005053
      contents = contents.split("n").map((text) ->
        <p>{text}</p>
      )

      @props.onSubmit.call(@, title, contents)
      @setState(
        title: ""
        contents: ""
      )

  render: ->
    <div className="card-control">
      <form onSubmit={@handleSubmit}>
        <input 
          type="text"
          className="card-control__input"
          placeholder="Card title."
          valueLink={@linkState("title")} />
        <textarea
          className="card-control__input"
          placeholder="Card contents."
          valueLink={@linkState("contents")}>
        </textarea>
        <button className="card-control__submit">Add!!</button>
      </form>
    </div>
)


# カード
Card = React.createClass(
  propTypes:
    index   : React.PropTypes.number.isRequired
    title   : React.PropTypes.string.isRequired
    contents: React.PropTypes.string.isRequired
    onClick : React.PropTypes.func.isRequired

  handleClick: (e) ->
    onClick = @props.onClick.call(@, @props.index)

  render: ->
    <div className="card-list__item card">
      <button className="card__delete" onClick={@handleClick}>&times;</button>
      <div className="card__title">{@props.title}</div>
      <div className="card__body">{@props.contents}</div>
    </div>
)


# ルートコンポーネント
App = React.createClass(

  # 今回は適当なデータを用意
  getInitialState: ->
    cards: [
      {title: "リストのアイテムについて", contents:"タイトル横のバツ印をクリックで削除できます。"}
      {title: "リストの追加方法", contents:"ページ上部のフォームからタイトルと内容を記載後、「Add!!」をクリックします。"}
    ]

  handleAdd: (title, contents) ->
    cards = @state.cards.slice()
    cards.push(
      title: title
      contents: contents
    )
    @setState(cards: cards)

  handleRemove: (index) ->
    cards = @state.cards.slice()
    cards.splice(index, 1)
    @setState(cards: cards)

  # <CSSTransitionGroup />の中にカードを描画する
  render: ->
    <div className="container">
      <h1>React transition sample</h1>
      <CardControl onSubmit={@handleAdd} />
      <div className="card-list">
        <CSSTransitionGroup transitionName="fademove">
          {@renderCards()}
        </CSSTransitionGroup>
      </div>
    </div>

  renderCards: ->
    @state.cards.map((card, i) =>
      <Card
        index={i}
        title={card.title}
        contents={card.contents}
        key={card.title} #本来はユニークな値を指定する
        onClick={@handleRemove} />
    )
)

React.render(<App />, document.getElementById("app"))

各コンポーネントを分割せずに一つのファイルにしているため少し長くみえますが、サンプルなので良いかなと…。

CSSTransitionGroupを使ってアニメーションをつける

上記のAppコンポーネントrender()内で指定している<CSSTransitionGroup />がアニメーション用のコンポーネントです。
React.jsAddonとして提供されているみたいです。

transitionName属性に任意の文字列を指定する必要があり、サンプルではfademoveを指定しています。
また、子にはそれぞれユニークなキー(key)を割り当てる必要があります。
ついていない場合でも、エラーにはなりませんがwarningが出てきますので注意が必要ですね。

アニメーションの前準備

React = require("react/addons")
CSSTransitionGroup = React.addons.CSSTransitionGroup

# ...

# ルートコンポーネント
App = React.createClass(

  # ...

  # <CSSTransitionGroup />の中にカードを描画する
  render: ->
    <div className="container">
      <h1>React transition sample</h1>
      <CardControl onSubmit={@handleAdd} />
      <div className="card-list">
        <CSSTransitionGroup transitionName="fademove">
          {@renderCards()}
        </CSSTransitionGroup>
      </div>
    </div>

  # ...
)

アニメーションの定義

実際のアニメーションを定義していきます。
先ほどtransitionNameに指定した文字列に、一定のタイミングでenterleaveが付与されるので、そこへスタイルを設定する仕組みです。

それぞれのタイミングは下記のようになります。

  • "任意の文字列"-enter:表示開始前
  • "任意の文字列"-enter-active:表示開始終了時のスタイル
  • "任意の文字列"-leave:表示終了前
  • "任意の文字列"-leave-active:表示終了時のスタイル
.fademove-enter {
  opacity:.01;
  transform:translateY(50%) scale(.95,.95);
  transition:all 1s ease-out;
  &#{&}-active {
    opacity:1;
    transform:translateY(0) scale(1,1);
  }
}

.fademove-leave {
  opacity:1;
  transform:translateY(0) scale(1,1);
  transition:all .2s ease-out;
  &#{&}-active {
    opacity:.01;
    transform:translateY(5%) scale(.98,.98);
  }
}

上記ではSCSS記法で書いてしまっていますので、素のCSSを使う場合は下記の様に書いていきます。

.fademove-enter {
  opacity:.01;
}

.fademove-enter.fademove-enter-active {
    opacity:1;
}

これで、追加と削除のタイミングで「ふわっとした」アニメーションがつきました!

番外編:初期表示時にアニメーションをつける

上記のサンプルでは、リストの初期描画がパッと表示されています。
しかし、最初もアニメーションをさせたい!という場合は、CSSTransitionGroupコンポーネントのtransitionAppeartrueを指定することで実現できます。
CSSはenterleaveと同様に定義します。

<CSSTransitionGroup transitionName="fademove" transitionAppear={true}>
  {@renderCards()}
</CSSTransitionGroup>
.fademove-appear {
  opacity:.01;
  transform:translateY(50%) scale(.95,.95);
  transition:all 1s ease-out;
  &#{&}-active {
    opacity:1;
    transform:translateY(0) scale(1,1);
  }
}

これで下記のように、最初表示されるときもふわっとアニメーションがつきました。

初期表示でアニメーションした例

(分かりづらいですがブラウザを更新しています…)

まとめ

データを扱う以上、一覧で見せることは非常に多いので、ちょっとだけいい感じに見せたいんだよね〜という場合に使えそうな感じがします。

また、今回使ったAddonであるCSSTransitionGroupですが、ng-animateというAngularJSのライブラリに影響を受けて作られているらしいので、AngularJSに詳しい方だとさっと使いこなせそうですね!
(恥ずかしながら僕はAngularJS触ったことありません…)

参考サイト