序章

Downshift は、シンプルで柔軟性のある、WAI-ARIA準拠の拡張入力Reactコンポーネントの構築を支援するライブラリです。 その主なユースケースはオートコンプリートコンポーネントの構築ですが、ドロップダウンコンポーネントの構築にも使用できます。

このチュートリアルでは、ダウンシフトで解決されるいくつかの一般的なユースケースについて説明します。

前提条件

このチュートリアルに従うには、マシンにNodeとNPMがインストールされている必要があります。 Reactの基本的な理解は、このチュートリアルを最大限に活用するのに役立ちます。

Node.jsがインストールされていない場合は、 Node Webサイトにアクセスして、オペレーティングシステムに推奨されるバージョンをインストールしてください。

ステップ1—アプリケーションのセットアップ

Create React Appを利用して、ビルド構成のないシンプルなReactアプリを作成します。 Create React Appがインストールされていない場合は、端末でnpm i create-react-appを実行してインストールしてください。 マシンにインストールしたら、次のコマンドを実行してdownshift-examplesというディレクトリに新しいReactプロジェクトをセットアップし、次のコマンドを実行してこの新しいディレクトリに移動します。

  1. create-react-app downshift-examples
  2. cd downshift-examples

downshift-examplesディレクトリに移動したら、次のコマンドを実行して、Downshiftおよびその他のパッケージをインストールします。

  1. yarn add downshift axios react-popper

srcフォルダーにあるApp.cssファイルを開き、次のスタイルを追加します。

src / App.css
    input {
      margin: 1rem;
      width: 20rem;
      padding: 1rem .5rem;
    }
    .downshift-dropdown {
      margin: 0 auto;
      width: 20rem;
      border: 1px solid whitesmoke;
      border-bottom: none;
    }
    .dropdown-item {
      padding: 0.5rem;
      cursor: pointer;
      border-bottom: 1px solid whitesmoke;
      font-size: 1rem;
      text-align: left;
    }
    .dropdown-button {
      padding: 0.6rem;
      border-radius: 3px;
      background: white;
      cursor: pointer;
    }
    .popper-div {
      flex: 1;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-top: 5rem;
    }
    .popper-item {
      padding: .5rem;
      border-bottom: 1px solid whitesmoke;
    }

すべてがセットアップされたら、いくつかの基本的なダウンシフトの概念を見てみましょう。

ダウンシフトの概念の概要

ダウンシフトを使用する場合、必要なコンポーネントは<Downshift />のみです。 <Downshift />コンポーネントと呼び、いくつかの小道具を渡すと、魔法のように機能します。 最もよく使われる小道具のいくつかを次に示します。

  1. onChange:この関数は、ユーザーがアイテムを選択し、選択したアイテムが変更されたときに呼び出されます。 selectedItemを返します。
  2. itemToString:この関数は、inputValueの計算に使用される選択されたアイテムの文字列値を決定するために使用されます。
  3. inputValue:これは入力フィールドが持つべき値を表します。
  4. getInputProps:この関数は、レンダリングするinput要素に適用する必要のある小道具を返します。
  5. getItemProps:この関数は、レンダリングするメニュー項目要素に適用する必要のある小道具を返します。
  6. isOpen:これはメニューが開いているかどうかを示すブール値です。
  7. selectedItem:これは現在選択されているアイテム入力を表します。
  8. render:これは、ダウンシフトの状態に基づいて、必要なものをレンダリングする場所です。 この関数はオブジェクトとともに呼び出されます。

小道具の完全なリストについては、ドキュメントを確認できます。 それでは、この知識を活用しましょう。

ステップ2—選択フィールドの作成

最初のダウンシフトのユースケースは、選択フィールドです。 先に進み、アプリのルートディレクトリのsrcフォルダーにDownshiftOne.jsファイルを作成します。 次のコードを追加します。

src / DownshiftOne.js
    import React from 'react'
    import Downshift from 'downshift';

    const books = [
      { name: 'Harry Potter' },
      { name: 'Net Moves' },
      { name: 'Half of a yellow sun' },
      { name: 'The Da Vinci Code' },
      { name: 'Born a crime' },
    ];

    const onChange = (selectedBook) => {
      alert(`your favourite book is ${selectedBook.name}`)
    }

    export default () => {
      return (
        <Downshift onChange={onChange} itemToString={books => (books ? books.name : '')}>
          {/* we'll insert a callback here */}
        </DownShift>
      )
    }

上記のコードでは、ReactとDownshiftをインポートし、本の配列、onChange関数、および<Downshift/>コンポーネントを返す関数コンポーネントを宣言します。 <Downshift/>コンポーネントでは、onChangeおよびitemToStringプロップを渡します。 <Downshift/>コンポーネント内で、他の小道具をコールバックに渡し、入力フィールドをレンダリングします。

次に、コールバックで必要な小道具を<Downshift/>コンポーネントに渡します。 機能コンポーネントを次のように更新します。

src / DownshiftOne.js
    ...

    export default () => {
      return (
        <Downshift onChange={onChange} itemToString={books => (books ? books.name : '')}>
          // pass the downshift props into a callback
          {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex, selectedItem, getLabelProps }) => (
            <div>
              // add a label tag and pass our label text to the getLabelProps function
              <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>Choose your favourite book</label> <br />
              // add our input element and pass our placeholder to the getInputProps function
              <input {...getInputProps({ placeholder: "Search books" })} />
              // if the input element is open, render the div else render nothing
              {isOpen ? (
                <div className="downshift-dropdown">
                  {
                    // filter the books and return items that match the inputValue
                    books
                      .filter(item => !inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase()))
                      // map the return value and return a div
                      .map((item, index) => (
                        <div
                          className="dropdown-item"
                          {...getItemProps({ key: item.name, index, item })}
                          style={{
                            backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                            fontWeight: selectedItem === item ? 'bold' : 'normal',
                          }}>
                          {item.name}
                        </div>
                      ))
                  }
                </div>
              ) : null}
            </div>
          )}
        </Downshift>
      )
    }

上記のコードスニペットでは、ダウンシフトプロップをパラメーターとしてコールバックに渡しました。

    {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex, selectedItem, getLabelProps }) => ()}

コールバックでは、入力タグを追加し、それにgetInputProps小道具を渡します。

    <input {...getInputProps({ placeholder: "Search books" })} />

次に、入力要素が開いているかどうかを確認します。 そうである場合は、メニューを含むdiv要素を返し、そうでない場合はnullを返します。

    { isOpen ? (<div className="downshift-dropdown">...</div>) : null }

最後に、返すdiv要素で、books配列をフィルタリングして、inputValueを含むアイテムのみを返します。 次に、フィルタリングされた本をマッピングして、ページにレンダリングします。

また、getItemProps関数をmap関数でレンダリングされたdivに渡しました。 アイテムのレンダリング中に適用した小道具を返します。

コンポーネントを親Appコンポーネントにインポートして、機能選択フィールドを確認してみましょう。

src / App.js
    import React, { Component } from 'react';
    import logo from './logo.svg';
    import './App.css';
    import DownshiftOne from './DownshiftOne'; // import the component

    class App extends Component {
      render() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h1 className="App-title">Welcome to React</h1>
            </header>
            <DownshiftOne /> // render the component
          </div>
        );
      }
    }
    export default App;

ターミナルでnpm startを実行して、サーバーが実行されていることを確認します。 ブラウザでhttp://localhost:3000を開くと、アプリが実行されていることがわかります。

ステップ3—Axiosでダウンシフトを使用する

次の例では、Downshiftを使用して映画の検索フィールドを作成します。 srcフォルダーに、DownshiftTwo.jsファイルを作成し、次のコードを追加します。

src / DownshiftTwo.js
    import React, { Component } from 'react'
    import Downshift from 'downshift';
    import axios from 'axios';

    export default class DownshiftTwo extends Component {
      constructor(props) {
        super(props)
        this.state = {
          movies: []
        }
        this.fetchMovies = this.fetchMovies.bind(this)
        this.inputOnChange = this.inputOnChange.bind(this)
      }
      // onChange method for the input field
      inputOnChange(event) {
        if (!event.target.value) {
          return
        }
        this.fetchMovies(event.target.value)
      }
      // input field for the <Downshift /> component
      downshiftOnChange(selectedMovie) {
        alert(`your favourite movie is ${selectedMovie.title}`)
      }
      // method to fetch the movies from the movies API
      fetchMovies(movie) {
        const moviesURL = `https://api.themoviedb.org/3/search/movie?api_key=APIKey&query=${movie}`;
        axios.get(moviesURL).then(response => {
          this.setState({ movies: response.data.results })
        })
      }
      render() {
        return (
          <Downshift onChange={this.downshiftOnChange} itemToString={item => (item ? item.title : '')}>
          // pass the downshift props into a callback
            {({ selectedItem, getInputProps, getItemProps, highlightedIndex, isOpen, inputValue, getLabelProps }) => (
              <div>
                // add a label tag and pass our label text to the getLabelProps function
                <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>Choose your favourite movie</label> <br />
                // add a input tag and pass our placeholder text to the getInputProps function. We also have an onChange eventlistener on the input field
                <input {...getInputProps({
                  placeholder: "Search movies",
                  onChange: this.inputOnChange
                })} />
                // if the input element is open, render the div else render nothing
                {isOpen ? (
                  <div className="downshift-dropdown">
                    {
                      // filter the movies in the state
                      this.state.movies
                        .filter(item => !inputValue || item.title.toLowerCase().includes(inputValue.toLowerCase()))
                        .slice(0, 10) // return just the first ten. Helps improve performance
                        // map the filtered movies and display their title
                        .map((item, index) => (
                          <div
                            className="dropdown-item"
                            {...getItemProps({ key: index, index, item })}
                            style={{
                              backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                              fontWeight: selectedItem === item ? 'bold' : 'normal',
                            }}>
                            {item.title}
                          </div>
                        ))
                    }
                  </div>
                ) : null}
              </div>
            )}
          </Downshift>
        )
      }
    }

上記のコードには、<Downshift/>コンポーネントをレンダリングし、それに小道具を渡すクラスコンポーネントがあります。 <Downshift/>コンポーネントの入力フィールドには、フィールドへの新しい入力をリッスンするonChangeイベントリスナーがあります。

入力がある場合は、fetchMoviesメソッドを呼び出します。このメソッドは、入力フィールドの値を取得し、Axiosを使用してmoviesAPIにAJAXリクエストを送信します。 前の例で行ったように、要求応答をコンポーネントの状態に設定し、それらをフィルター処理して一致するアイテムを返します。

前の例で行ったように、親Appコンポーネントのコンポーネントをインポートしてレンダリングします。 ブラウザにアクセスして、お気に入りの映画を検索してみてください。

ステップ4—ダウンシフトを使用したドロップダウンの作成

ダウンシフトのもう1つの使用例は、ドロップダウンへの電力供給です。 ドロップダウンのAPIは、単純なドロップダウンコンポーネントの構築に役立ちます。 srcフォルダにDownshiftThree.jsファイルを作成し、これを実現する方法を見てみましょう。

DownshiftThree.jsファイルに、次のコードを追加します。

src / DownshiftThree.js
    import React, { Component } from 'react'
    import Downshift from 'downshift';

    export default class DownshiftThree extends Component {
      constructor(props) {
        super(props)
        this.books = [
          { name: 'Harry Potter' },
          { name: 'Net Moves' },
          { name: 'Half of a yellow sun' },
          { name: 'The Da Vinci Code' },
          { name: 'Born a crime' },
        ];

        this.state = {
          // currently selected dropdown item
          selectedBook: ''
        }

        this.onChange = this.onChange.bind(this)
      }

      onChange(selectedBook) {
        this.setState({ selectedBook: selectedBook.name })
      }

      render() {
        return (
          <Downshift onChange={this.onChange} selectedItem={this.state.selectedBook} itemToString={books => (books ? books.name : '')}>
          // pass the downshift props into a callback
            {({ isOpen, getToggleButtonProps, getItemProps, highlightedIndex, selectedItem: dsSelectedItem, getLabelProps }) => (
              <div>
                // add a label tag and pass our label text to the getLabelProps function
                <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>Select your favourite book</label> <br />
                // add a button for our dropdown and pass the selected book as its content if there's a selected item
                <button className="dropdown-button" {...getToggleButtonProps()}>
                  {this.state.selectedBook !== '' ? this.state.selectedBook : 'Select a book ...'}
                </button>
                <div style={{ position: 'relative' }}>
                  // if the input element is open, render the div else render nothing
                  {isOpen ? (
                    <div className="downshift-dropdown">
                      {
                        // map through all the books and render them
                        this.books.map((item, index) => (
                          <div
                            className="dropdown-item"
                            {...getItemProps({ key: index, index, item })}
                            style={{
                              backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                              fontWeight: dsSelectedItem === item ? 'bold' : 'normal',
                            }}>
                            {item.name}
                          </div>
                        ))
                      }
                    </div>
                  ) : null}
                </div>
              </div>
            )}
          </Downshift>
        )
      }
    }

上記のコードには、DownshiftThreeクラスコンポーネントがあり、<Downshift/>コンポーネントをレンダリングします。 渡されるコールバックには、getToggleButtonProps関数を渡すボタンがあります。 ボタンには、コンポーネントの状態のselectedBookが設定されているかどうかに基づいて、ボタンのコンテンツを設定する三項演算子が含まれています。

次に、isOpenプロップを呼び出して、ドロップダウンが開いているかどうかを確認します。 開いている場合は、すべての本をマップして、ドロップダウンに表示します。

<Downshift/>コンポーネントに渡されるonChangeメソッドでは、アイテムが選択されるたびに状態が設定され、ボタンのコンテンツが更新されます。 コンポーネントをインポートして親Appコンポーネントにレンダリングし、ブラウザーをリロードして、この時点でアプリを表示します。

ステップ5—ダウンシフトを使用したフォームの作成

この例では、フォームの入力フィールドとしてダウンシフト入力コンポーネントを使用し、フォームデータの送信を試みます。 srcディレクトリに、DownshiftInputField.jsDownshiftFour.jsの2つのファイルを作成しましょう。

DownshiftInputField.jsでは、Downshiftを使用して入力コンポーネントを作成し、それを使用してDownshiftFour.jsファイルのいくつかの入力フィールドをレンダリングします。 DownshiftInputField.jsファイルに機能コンポーネントを作成しましょう。

src / DownshiftInputField.js
    import React from 'react'
    import Downshift from 'downshift';

    export default ({ items, onChange, label, placeholder, name }) => {
      return (
        <Downshift onChange={onChange} itemToString={items => (items ? items.name : '')}>
          {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex, selectedItem, getLabelProps }) => (
            <div>
            // add a label tag and pass our label text to the getLabelProps function
              <label style={{ marginTop: '1rem', display: 'block' }} {...getLabelProps()}>{label}</label> <br />
              // add an input tag and pass our placeholder text to the getInputProps function
              <input name={name} {...getInputProps({ placeholder })} />
              // if the input element is open, render the div else render nothing
              {isOpen ? (
                <div className="downshift-dropdown">
                  {
                    items
                      // filter the items and return those that includes the inputValue
                      .filter(item => !inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase()))
                      // map through the returned items and render them to the page
                      .map((item, index) => (
                        <div
                          className="dropdown-item"
                          {...getItemProps({ key: item.name, index, item })}
                          style={{
                            backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                            fontWeight: selectedItem === item ? 'bold' : 'normal',
                          }}>
                          {item.name}
                        </div>
                      ))
                  }
                </div>
              ) : null}
            </div>
          )}
        </Downshift>
      )
    }

上記のコードでは、機能コンポーネントはアイテムの配列、onChange関数、ラベルとプレースホルダーテキスト、そして最後に名前を受け取ります。 コンポーネントは、コールバック関数で必要なすべての小道具を受け取る<Downshift/>コンポーネントを返します。 コールバックには、ラベルと入力フィールドがあります。

他の例と同様に、isOpenプロップを三項演算子に渡します。 入力フィールドが開いている場合は、アイテムの配列をフィルタリングしてinputValueに一致するアイテムを返し、返されたアイテムをマップしてDOMにレンダリングします。

入力フィールドコンポーネントの準備ができたので、それをDownshiftFour.jsファイルにインポートしましょう。

src / DownshiftFour.js
    import React, { Component } from 'react'
    import DownshiftInputField from './DownshiftInputField';

    export default class DownshiftFour extends Component {
      constructor(props) {
        super(props)
        this.state = {
          books: [
            { name: 'Harry Potter' },
            { name: 'Net Moves' },
            { name: 'Half of a yellow sun' },
            { name: 'The Da Vinci Code' },
            { name: 'Born a crime' },
          ],
          movies: [
            { name: 'Harry Potter' },
            { name: '12 Strong' },
            { name: 'Half of a yellow sun' },
            { name: 'Gringo' },
            { name: 'Black Panther' },
          ],
          book: '',
          movie: ''
        }
        this.onSubmit = this.onSubmit.bind(this);
        this.onChange = this.onChange.bind(this);
      }

      onSubmit(event) {
        event.preventDefault();
        alert(`
        Favourite book: ${this.state.book}
        Favourite movie: ${this.state.movie}
        has been submitted
        `)
      }

      onChange(selectedBook, stateAndHelpers) {
        const element = document.querySelector(`#${stateAndHelpers.id}-input`)
        this.setState({ [element.name]: selectedBook.name })
      }

      render() {
        return (
          <form onSubmit={this.onSubmit}>
            <DownshiftInputField
              items={this.state.books}
              onChange={this.onChange}
              label="Select your favourite book"
              name="book"
              placeholder="Search your favourite book" />
            <DownshiftInputField
              items={this.state.movies}
              onChange={this.onChange}
              label="Select your favourite movie"
              name="movie"
              placeholder="Search your favourite movie" />
            <input type="submit" value="Submit" className="dropdown-button" />
          </form>
        )
      }
    }

DownshiftFour.jsファイルで、入力フィールドコンポーネントをインポートし、クラスコンポーネントを作成しました。 コンポーネントの状態では、本と映画の配列があり、入力フィールドコンポーネントをフォームで2回レンダリングします。1つは本の入力フィールド、もう1つは映画の入力フィールドです。

DownshiftのonChange関数は、stateAndHelpersと呼ばれる2番目のパラメーターを取り込んで、Downshiftの現在の状態に関する情報を提供します。

onChangeメソッドでは、stateAndHelpers引数からidを取得し、DOMにクエリを実行することで、ユーザーが現在操作している入力フィールドを見つけます。 要素を取得したら、それをコンポーネント状態に設定します。

ユーザーが送信ボタンを押すと、onSubmitメソッドは、選択されたbookmovieを状態から取得し、必要な処理を実行します。

親のAppコンポーネントに<DownshiftFour/>コンポーネントをインポートしてレンダリングし、ブラウザで回転させましょう。

ステップ6—Popper.jsでダウンシフトを使用する

この記事の最後の例では、Popper.jsを使用してダウンシフトポップアップの方向を変更します。 Popperは、ポジショニングツールチップまたはポップアップをより管理しやすいタスクにするライブラリです。

アプリケーションのセットアップ時にreact-popperパッケージを既にインストールしているので、srcフォルダーにDownshiftFive.jsファイルを作成しましょう。

次のコードを新しいファイルに追加しましょう。

src / DownshiftFive.js
    import React, { Component } from 'react'
    import Downshift from 'downshift';
    import { Popper, Manager, Target } from 'react-popper';

    export default class DownshiftFive extends Component {
      // define some default props
      static defaultProps = {
        positions: [
          'top',
          'top-start',
          'top-end',
          'bottom-start',
          'bottom',
          'bottom-end',
        ]
      }

      render() {
        return (
          <div className="popper-div">
          // wrap the whole component in Popper's <Manager/> component
            <Manager>
              <Downshift render={({ inputValue, getInputProps, getItemProps, isOpen, selectedItem, highlightedIndex }) => (
                <div>
                // wrap our input element in Popper's <Target/> component
                  <Target>
                    <input {...getInputProps({ placeholder: 'Enter a position' })} />
                  </Target>
                  <div className="downshift-dropdown">
                    {isOpen ? (
                    // pass the selected item to Popper
                      <Popper
                        placement={selectedItem || 'bottom'}
                        style={{ backgroundColor: 'skyblue' }}
                      >
                        {
                        // filter through all the positions and return the ones that include the inputValue
                          this.props.positions
                            .filter(item => !inputValue || item.includes(inputValue.toLowerCase()))
                            // map through all the returned positions and render them
                            .map((item, index) => (
                              <div className="downshift-item popper-item"
                                {...getItemProps({ item })}
                                key={item}
                                style={{
                                  cursor: 'pointer',
                                  backgroundColor: highlightedIndex === index ? '#bed5df' : 'transparent',
                                  fontWeight: selectedItem === item ? 'bold' : 'normal',
                                }}>
                                {item}
                              </div>
                            ))
                        }
                      </Popper>
                    ) : null}
                  </div>
                </div>
              )}></Downshift>
            </Manager>
          </div>
        )
      }
    }

上記のコードスニペットでは、デフォルトのpositionプロップを使用してDownshiftFiveクラスコンポーネントを作成します。 これらは、Popperがポップアップをレンダリングするために使用する位置です。 コンポーネントのrenderメソッドで、<Manager/>コンポーネントにラップされた<Downshift/>コンポーネントを返します。

    return (
      <div className="popper-div">
        <Manager>
          <Downshift render={({ inputValue, getInputProps, getItemProps, isOpen, selectedItem, highlightedIndex }) => (
            {/* all of our remaining code goes here */}
          )}>
          </DownShift>
        </Manager>
      </div>
    )

Managerコンポーネントは、Popperによって公開されるラッパーであり、ページ上の他のすべてのreact-popperコンポーネントを囲んで、それらを相互に通信させる必要があります。

よく見ると、renderプロップが<Downshift/>コンポーネントに渡されていることがわかります。 これは、小道具を<Downshift/>コンポーネントに渡すもう1つの方法です。 基本的に、コールバックを移動してrenderプロップに渡しました。

renderプロップに渡されるコールバックでは、入力フィールドを<Target/>コンポーネントでラップします。 これは、これがポップアップがレンダリングされる入力フィールドであることをPopperに通知します。

次に、入力が開いているかどうかを確認し、<Popper/>コンポーネントをレンダリングして、selectedItemplacementプロップに渡します。 これは、新しい位置が選択されるたびに、ポッパーがポップアップを再配置するのに役立ちます。 最後に、他の例と同様に、すべてのデフォルトの小道具の位置をフィルタリングし、inputValueを含む位置を返し、それらをマッピングしてページにレンダリングします。

最後に、親のAppコンポーネントに<DownshiftFive/>コンポーネントをインポートしてレンダリングし、ブラウザでチェックアウトします。 最終製品が表示されます。

結論

この投稿では、Downshiftで解決される一般的なReactドロップダウンのユースケースを調査するためのアプリケーションを作成しました。