iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Modern Web

用React刻自己的投資Dashboard系列 第 12

用React刻自己的投資Dashboard Day12 - 下拉式選單篩選功能

tags: 2021鐵人賽 React

還記得這個網站有篩選圖表的功能嗎?當初畫wireframe的時候考量到未來圖表可能會越來越多,因此規劃了下拉式選單來做篩選功能,如下圖:

本篇就來用React寫寫看這個篩選功能吧!

準備下拉式選單選項

先來看看"美國製造業電子零件訂單"這個series,在FRED網站上是怎麼分類的,如下圖:

可以看到下面有一些比較詳細的資料,這邊把它放大一下,裡面包含幾項內容:

  • Source:資料來源
  • Release:報告名稱
  • Units:單位
  • Frequency:報告發布頻率

這邊稍微解釋一下series、source、release三者的關係,series是指時間序列資料,例如"美國製造業電子零件訂單"這個series是由過去每個月的數據組成,將這些數據依照時間排序就是一個序列,而一個series來自一個release,一個source通常有多個releases,一個release可能來自多個sources。

當要做篩選功能的時候,可以做的篩選方式可能就會有三種方式:

  • 透過source篩選
  • 透過release篩選
  • 同時篩選source及release

因此首先下拉式選單內要有可以選擇的source與release,這些資料可以從FRED API取得,不過如果把API可以拿到的所有sources與releases放進來的話有點太多,因此我先依照目前有用到的series去列出有用到的sources與releases就好,製作成sources.json與releases.json如下:

src\data\sources.json

[
  {
    "id": 1,
    "realtime_start": "2021-08-22",
    "realtime_end": "2021-08-22",
    "name": "Board of Governors of the Federal Reserve System (US)",
    "link": "http://www.federalreserve.gov/"
  },
  {
    "id": 19,
    "realtime_start": "2021-08-22",
    "realtime_end": "2021-08-22",
    "name": "U.S. Census Bureau",
    "link": "http://www.census.gov/"
  }
]

src\data\releases.json

[
  {
    "id": 18,
    "realtime_start": "2021-08-22",
    "realtime_end": "2021-08-22",
    "name": "H.15 Selected Interest Rates",
    "press_release": true,
    "link": "http://www.federalreserve.gov/releases/h15/"
  },
  {
    "id": 20,
    "realtime_start": "2021-08-22",
    "realtime_end": "2021-08-22",
    "name": "H.4.1 Factors Affecting Reserve Balances",
    "press_release": true,
    "link": "http://www.federalreserve.gov/releases/h41/"
  },
  {
    "id": 95,
    "realtime_start": "2021-08-22",
    "realtime_end": "2021-08-22",
    "name": "Manufacturer's Shipments, Inventories, and Orders (M3) Survey",
    "press_release": true,
    "link": "http://www.census.gov/indicator/www/m3/"
  }
]

series新增來源編碼

原本的series資料只有release_id去辨識,為了希望能用來源去篩選,因此要在series資料內新增source_id辨識來源:

src\data\chart-collections.json

[
  {
    "id": 1,
    "series_id": "TREAST",
    "release_id": 20,
    // 新增source id
    "source_id": 1,
    ...
  },
  {
    "id": 2,
    "series_id": "DGS10",
    "release_id": 95,
    // 新增source id
    "source_id": 1,
    ...
  },
  {
    "id": 3,
    "series_id": "A34HNO",
    "release_id": 18,
    // 新增source id
    "source_id": 19,
    ...
  }
]

製作下拉式選單

既然我們有了sources.json與releases.json,就可以把這些資料丟給React,這邊有兩個可以import資料的選擇,一個是從最上層的App.js引入,另外一個是從選單元素Selector.js引入,考量到之後可能會在除了下拉式選單的地方用到這些資料,因此選擇從App.js引入,再透過props傳遞到子元件。

從App引入資料,並透過props傳遞給Selector元件。

src\App.js

import Navbar from './components/Navbar/Navbar';
import Selector from './components/Selector/Selector';
import Charts from './components/Charts/Charts';
import chartCollections from './data/chart-collections.json';
// import 選單資料
import releases from './data/releases.json';
import sources from './data/sources.json';

function App() {
  return (
    <div className="App">
      <Navbar />
      <Selector
        releases={releases}
        sources={sources}
      />
      <Charts charts={chartCollections} />
    </div>
  );
}

export default App;

在Selector元件接收props,並使用list render製作下拉式選單的option。

src\components\Selector\Selector.js

import React from 'react';
import styles from './Selector.module.css';
import Form from 'react-bootstrap/Form';
import { Row, Col } from 'react-bootstrap';

const Selector = (props) => {
  return <Row className={styles.selector}>
    <Col sm={12} md={6} lg={4} className={styles.selectorItem}>
      <Form.Select
        aria-label="Default select example"
      >
        <option value={0}>All Sources</option>
        {props.sources.map((e) => (
          <option value={e.id} key={e.id}>{e.name}</option>
        ))}
      </Form.Select>
    </Col>
    <Col sm={12} md={6} lg={4} className={styles.selectorItem}>
      <Form.Select
        aria-label="Default select example"
      >
        <option value={0}>All Releases</option>
        {props.releases.map((e) => (
          <option value={e.id} key={e.id}>{e.name}</option>
        ))}
      </Form.Select>
    </Col>
  </Row>
};

export default Selector;

使用state儲存選到的source_id及release_id

這次的下拉式選單功能,想做到讓使用者選取項目後,不需要再按一個submit的按鈕,下方的圖表就會自動篩選。為了要做到這個功能,首先要讓React知道使用者選了什麼選項,如果選項有改變,React也要馬上啟動篩選機制。

因此,我想到了使用state去追蹤下拉式選單的狀態,也就是讓React記住當前選到的source_id及release_id,當使用者操作下拉式選單,就會去調整對應的state,再根據調製後的state去篩選圖表。

建立State

建立filteredReleaseId與filteredSourceId兩個State,並且透過props傳遞給Selector元件。

src\App.js

...

function App() {
  const [filteredReleaseId, setFilteredReleaseId] = useState(0);
  const [filteredSourceId, setFilteredSourceId] = useState(0);

  return (
    <div className="App">
      <Navbar />
      <Selector
        selectedReleaseId={filteredReleaseId}
        selectedSourceId={filteredSourceId}
        releases={releases}
        sources={sources}
      />
      <Charts charts={chartCollections} />
    </div>
  );
}

export default App;

Selector接收props

在Form.Select設定defaultValue為props傳遞進來的資料。

src\components\Selector\Selector.js

...

const Selector = (props) => {
  return <Row className={styles.selector}>
    <Col sm={12} md={6} lg={4} className={styles.selectorItem}>
      <Form.Select
        aria-label="Default select example"
        defaultValue={props.selectedSourceId}
      >
        <option value={0}>All Sources</option>
        {props.sources.map((e) => (
          <option value={e.id} key={e.id}>{e.name}</option>
        ))}
      </Form.Select>
    </Col>
    <Col sm={12} md={6} lg={4} className={styles.selectorItem}>
      <Form.Select
        aria-label="Default select example"
        defaultValue={props.selectedReleaseId}
      >
        <option value={0}>All Releases</option>
        {props.releases.map((e) => (
          <option value={e.id} key={e.id}>{e.name}</option>
        ))}
      </Form.Select>
    </Col>
  </Row>
};

export default Selector;

透過state篩選圖表

可以透過filteredReleaseId與filteredSourceId去篩選圖表資料,方式為:建立filteredCharts儲存篩選後符合條件的資料,再將其傳遞到Charts元件。

src\App.js

...
function App() {
  const [filteredReleaseId, setFilteredReleaseId] = useState(0);
  const [filteredSourceId, setFilteredSourceId] = useState(0);
  
  // 篩選圖表的方式
  const filteredCharts = chartCollections.filter(chart => {
    if (filteredReleaseId === 0 && filteredSourceId === 0) return true
    if (filteredReleaseId === 0 && filteredSourceId !== 0) {
      return chart.source_id === filteredSourceId
    }
    if (filteredReleaseId !== 0 && filteredSourceId === 0) {
      return chart.release_id === filteredReleaseId
    }
    if (filteredReleaseId !== 0 && filteredSourceId !== 0) {
      return chart.release_id === filteredReleaseId && chart.source_id === filteredSourceId
    }
  });

  return (
    <div className="App">
      <Navbar />
      <Selector
        selectedReleaseId={filteredReleaseId}
        selectedSourceId={filteredSourceId}
        releases={releases}
        sources={sources}
      />
      <Charts charts={filteredCharts} />
    </div>
  );
}

export default App;

下拉式選單新增事件,調用setState

上面的程式碼知道如何篩選圖表資料,接著,我們要讓使用者透過下拉式選單去改變filteredReleaseId與filteredSourceId這兩個state,就會促使React啟動篩選的程式。
作法是建立兩個setState函數,並將兩個函數傳遞給Selector元件,讓子元件可以呼叫函數修改父元件的state,這個過程稱為資料的逆向傳遞。

src\App.js

...

function App() {
  const [filteredReleaseId, setFilteredReleaseId] = useState(0);
  const [filteredSourceId, setFilteredSourceId] = useState(0);

  const filteredCharts = chartCollections.filter(chart => {
    if (filteredReleaseId === 0 && filteredSourceId === 0) return true
    if (filteredReleaseId === 0 && filteredSourceId !== 0) {
      return chart.source_id === filteredSourceId
    }
    if (filteredReleaseId !== 0 && filteredSourceId === 0) {
      return chart.release_id === filteredReleaseId
    }
    if (filteredReleaseId !== 0 && filteredSourceId !== 0) {
      return chart.release_id === filteredReleaseId && chart.source_id === filteredSourceId
    }
  });
  
  // setState函數
  const releaseIdChangeHandler = (selectedReleaseId) => {
    setFilteredReleaseId(selectedReleaseId);
  };
  // setState函數
  const sourceIdChangeHandler = (selectedSourceId) => {
    setFilteredSourceId(selectedSourceId);
  };

  return (
    <div className="App">
      <Navbar />
      <Selector
        selectedReleaseId={filteredReleaseId}
        selectedSourceId={filteredSourceId}
        releases={releases}
        sources={sources}
        onReleaseIdChange={releaseIdChangeHandler}
        onSourceIdChange={sourceIdChangeHandler}
      />
      <Charts charts={filteredCharts} />
    </div>
  );
}

export default App;

在Selector的Form.Select元件內新增onChange事件,再透過父元件傳遞進來的事件將篩選到的值逆向傳遞回去。這邊要注意的是子元件不能直接修改父元件的state,但是可以透過props將資料逆向傳遞回去,透過父元件的setState函數去修改父元件的state。

src\components\Selector\Selector.js

...

const Selector = (props) => {
  const releaseIdChange = (event) => {
    props.onReleaseIdChange(Number(event.target.value));
  };

  const sourceIdChange = (event) => {
    props.onSourceIdChange(Number(event.target.value));
  };

  return <Row className={styles.selector}>
    <Col sm={12} md={6} lg={4} className={styles.selectorItem}>
      <Form.Select
        aria-label="Default select example"
        defaultValue={props.selectedSourceId}
        onChange={sourceIdChange}
      >
        <option value={0}>All Sources</option>
        {props.sources.map((e) => (
          <option value={e.id} key={e.id}>{e.name}</option>
        ))}
      </Form.Select>
    </Col>
    <Col sm={12} md={6} lg={4} className={styles.selectorItem}>
      <Form.Select
        aria-label="Default select example"
        defaultValue={props.selectedReleaseId}
        onChange={releaseIdChange}
      >
        <option value={0}>All Releases</option>
        {props.releases.map((e) => (
          <option value={e.id} key={e.id}>{e.name}</option>
        ))}
      </Form.Select>
    </Col>
  </Row>
};

export default Selector;

小結

看似簡單的篩選功能,其實也是蠻多程式碼的,不過藉此也學到資料如何在父元件與子元件間傳遞,算是React內非常基本的知識。

雖然目前有了篩選功能,但是當圖表越來越多張的時候,還是可能讓畫面變得很長,下一篇就來解決這個問題,最基本的方式應該就是用分頁來處理。


上一篇
用React刻自己的投資Dashboard Day11 - 分離UI元件與抓取數據元件
下一篇
用React刻自己的投資Dashboard Day13 - 製作分頁(Pagination)功能
系列文
用React刻自己的投資Dashboard30

尚未有邦友留言

立即登入留言