React-Redux小應用(一)-React_Redux_Appointment

React-Redux-Appointment

先來一波硬廣:個人博客歡迎觀光:傳送門
這個小應用使用Create React App建立,演示地址:https://liliang-cn.github.io/react_redux_appointment,repo地址:https://github.com/liliang-cn/react_redux_appointmentjavascript

這是以前的React_appointment的Redux版,以前的演示,改寫自Lynda的課程Building a Web Interface with React.jscss

文件結構

最終的文件目錄以下:html

react_redux_appointment/
  README.md
  node_modules/
  package.json
  public/
    index.html
    favicon.ico
  src/
    actions/
      index.js
    components/
      AddForm.js
      AptList.js
      Search.js
      Sort.js
    constants/
      index.js
    containers/
      AddForm.js
      App.js
    reducers/
      apts.js
      formExpanded.js
      index.js
      openDialog.js
      orderBy.js
      orderDir.js
      query.js
    index.css
    index.js

用到的模塊

{
  "name": "react_redux_appointment",
  "version": "0.1.0",
  "private": true,
  "homepage": "https://liliang-cn.github.io/react_redux_appointment",
  "devDependencies": {
    "react-scripts": "0.8.4"
  },
  "dependencies": {
    "axios": "^0.15.3",
    "gh-pages": "^0.12.0",
    "lodash": "^4.17.2",
    "material-ui": "^0.16.5",
    "moment": "^2.17.1",
    "react": "^15.4.1",
    "react-dom": "^15.4.1",
    "react-redux": "^5.0.1",
    "react-tap-event-plugin": "^2.0.1",
    "redux": "^3.6.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "deploy": "yarn build && gh-pages -d build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

全部的state

小應用一共有六個狀態,其中的formExpanded和openDialog是界面組件的狀態,
剩下的四個分別是apts(表明全部的預定)、orderBy(根據什麼來排列預定列表,根據姓名仍是根據日期)、
orderDir(排列列表的方向,是增序仍是降序)、query(搜索的關鍵字)。java

全部的Action

在應用中可能產生的actions有七種:node

  • addApt,即新建預定react

  • deleteApt, 即刪除預定ios

  • toggleDialog, 即顯示、隱藏警告框git

  • toggleFormExpanded, 顯示/隱藏表單github

  • query,即查詢json

  • changeOrderBy,即改變排序的關鍵字

  • changeOrderDir, 即改變排序方向

定義七個常量來表明這些action的類型:

constants/index.js:

export const ADD_APT = 'ADD_APT';

export const DELETE_APT = 'DELETE_APT';

export const TOGGLE_DIALOG = 'TOGGLE_DIALOG';

export const TOGGLE_FORM_EXPANDED = 'TOGGLE_FORM_EXPANDED';

export const QUERY = 'QUERY';

export const CHANGE_ORDER_BY = 'CHANGE_ORDER_BY';

export const CHANGE_ORDER_DIR = 'CHANGE_ORDER_DIR';

actions/index.js:

import {
    ADD_APT,
    DELETE_APT,
    TOGGLE_DIALOG,
    TOGGLE_FORM_EXPANDED,
    QUERY,
    CHANGE_ORDER_BY,
    CHANGE_ORDER_DIR
} from '../constants';

export const addApt = (apt) => ({
    type: ADD_APT,
    apt
});

export const deleteApt = (id) => ({
    type: DELETE_APT,
    id
});

export const toggleDialog = () => ({
    type: TOGGLE_DIALOG
});

export const toggleFormExpanded = () => ({
    type: TOGGLE_FORM_EXPANDED
});

export const query = (query) => ({
    type: QUERY,
    query
});

export const changeOrderBy = (orderBy) => ({
    type: CHANGE_ORDER_BY,
    orderBy
});

export const changeOrderDir = (orderDir) => ({
    type: CHANGE_ORDER_DIR,
    orderDir
});

UI組件

樣式

使用Material-UI須要引入Roboto字體:

src/index.css

@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
body {
  margin: 0;
  padding: 0;
  font-family: Roboto, sans-serif;
}

表單組件

components/addForm.js:

import React from 'react';

import {Card, CardHeader, CardText} from 'material-ui/Card';
import TextField from 'material-ui/TextField';
import DatePicker from 'material-ui/DatePicker';
import TimePicker from 'material-ui/TimePicker';
import RaisedButton from 'material-ui/RaisedButton';
import Paper from 'material-ui/Paper';
import Divider from 'material-ui/Divider';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';

import moment from 'moment';

const paperStyle = {
  width: 340,
  margin: '0 auto 20px',
  textAlign: 'center'
};

const buttonStyle = {
    margin: 12
};

// open, toggleDialog是兩個布爾值,handleAdd,formExpanded, toggleFormExpanded是三個回調函數,來自於../containers/AddForm.js中的容器從store中獲取並傳遞下來的
const AddForm = ({handleAdd, open, toggleDialog, formExpanded, toggleFormExpanded}) => {
    let guestName, date, time, note;
    // 點擊Add時會先首先檢查是否全部的值都有輸入,若是輸入合法則發起ADD_APT的action而後發起切換表單顯示的action,若是輸入有誤則發起TOGGLE_DIALOG的action
    const onAdd = () => {
        guestName && date && time && note
        ?
        handleAdd({guestName, date, time, note}) && toggleFormExpanded()
        :
        toggleDialog()
    };

    // 這兩個函數用來獲取輸入的日期和時間
    const handleDateChange = (event, aptDate) => {
        date = moment(aptDate).format('YYYY-MM-DD')
    };

    const handleTimeChange = (event, aptTime) => {
        time = moment(aptTime).format('hh:mm')
    };

    const actions = [
        <FlatButton
            label="OK"
            primary={true}
            onTouchTap={toggleDialog}
        />
    ];

    return (
        <Paper style={paperStyle} zDepth={2}>
            // Card組件的expanded的值是一個布爾值,來自於父組件傳下來的formExpanded,即應用的狀態formExpanded,用來肯定是否顯示錶單
            <Card style={{textAlign: 'left'}} expanded={formExpanded} onExpandChange={toggleFormExpanded}>
                <CardHeader
                    title="New Appointment"
                    showExpandableButton={true}
                />
                <CardText expandable={true}>
                    <TextField
                        floatingLabelText="Guest's Name"
                        underlineShow={false}
                        onChange={e => guestName = e.target.value.trim()}
                    />
                    <Divider />
                    <DatePicker
                        hintText="Date"
                        underlineShow={false}
                        onChange={handleDateChange}
                    />
                    <Divider />
                    <TimePicker
                        hintText="Time"
                        okLabel="OK"
                        cancelLabel="Cancel"
                        underlineShow={false}
                        onChange={handleTimeChange}
                    />
                    <Divider />
                    <TextField
                        floatingLabelText="Note"
                        underlineShow={false}
                        onChange={e => note = e.target.value.trim()}
                    />
                    <Divider />
                    <RaisedButton label="Add" primary={true} style={buttonStyle} onClick={onAdd}/>
                    <RaisedButton label="Cancel" secondary={true} style={buttonStyle} onClick={toggleFormExpanded}/>
                </CardText>
                // Dialog組件的open的值也是一個布爾值,來自於父組件傳下來的open,即應用的狀態openDialog,用來驗證表單
                <Dialog
                    title="Caution"
                    actions={actions}
                    modal={false}
                    open={open}
                    onRequestClose={toggleDialog}
                >
                    All fileds are required!
                </Dialog>
            </Card>
        </Paper>
    );
};

export default AddForm;

搜索表單

components/Search.js:

import React from 'react';
import TextField from 'material-ui/TextField';

const Search = ({handleSearch}) => {
    return (
        <div>
            <TextField
                hintText="Search"
                onChange={
                    e => handleSearch(e.target.value)
                }
            />
        </div>
    );
};

export default Search;

排列選擇

components/Sort.js:

import React from 'react';

import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem'

const Sort = ({
    orderBy,
    orderDir,
    handleOrderByChange,
    handleOrderDirChange
}) => {
    return (
        <div>
            <SelectField
                floatingLabelText="Order By"
                value={orderBy}
                style={{textAlign: 'left'}}
                onChange={(event, index, value) => {handleOrderByChange(value)}}
            >
                <MenuItem value='guestName' primaryText="Guest's name" />
                <MenuItem value='date' primaryText="Date" />
            </SelectField>

            <SelectField
                floatingLabelText="Order Direction"
                value={orderDir}
                style={{textAlign: 'left'}}
                onChange={(event, index, value) => {handleOrderDirChange(value)}}
            >
                <MenuItem value='asc' primaryText="Ascending" />
                <MenuItem value='desc' primaryText="Descending" />
            </SelectField>
        </div>
    );
};

export default Sort;

預定列表

這個組件的做用就是顯示預定列表,接受父組件傳來的apts數組和handleDelete函數,在點擊RaisedButton的時候將apt.id傳入handleDelete並執行。

components/AptList.js:

import React from 'react';
import {List, ListItem} from 'material-ui/List';
import {Card, CardActions, CardHeader, CardTitle, CardText} from 'material-ui/Card';
import RaisedButton from 'material-ui/RaisedButton';

const buttonStyle = {
    width: '60%',
    margin: '12px 20%',
};

const AptList = ({apts, handleDelete}) => {
    return (
        <div>
            <h2>Appointments List</h2>
            <List>
                // 這裏的i也能夠直接用apt.id
                {apts.map((apt, i) => (
                    <ListItem key={i}>
                        <Card style={{textAlign: 'left'}}>
                            <CardHeader
                                title={apt.date}
                                subtitle={apt.time}
                                actAsExpander={true}
                                showExpandableButton={true}
                            />
                            <CardTitle title={apt.guestName}/>
                            <CardText expandable={true}>
                                {apt.note}
                                <CardActions>
                                    <RaisedButton
                                        style={buttonStyle}
                                        label="Delete"
                                        secondary={true}
                                        onClick={() => handleDelete(apt.id)}
                                    />
                                </CardActions>
                            </CardText>
                        </Card>
                    </ListItem>
                ))}
            </List>
        </div>
    );
};

export default AptList;

處理不一樣的actions

處理表單的顯示和隱藏

reducers/formExpanded.js:

import { TOGGLE_FORM_EXPANDED } from '../constants';

// formExpanded默認爲false,即不顯示,當發起類型爲TOGGLE_FORM_EXPANDED的action的時候,將狀態切換爲true或者false
const formExpanded = (state=false, action) => {
    switch (action.type) {
        case TOGGLE_FORM_EXPANDED:
            return !state;
        default:
            return state;
    }
};

export default formExpanded;

表單驗證錯誤的提示對話框

reducers/openDialog.js:

import { TOGGLE_DIALOG } from '../constants';

// 這個action是由其餘action引起的
const openDialog = (state=false, action) => {
    switch (action.type) {
        case TOGGLE_DIALOG:
            return !state;
        default:
            return state;
    }
};

export default openDialog;

處理新建預定和刪除預定

reducers/apts.js:

import { ADD_APT, DELETE_APT } from '../constants';

// 用惟一的id來標識不一樣的預定,也能夠直接用時間戳new Date()
let id = 0;

// 根據傳入的數組和id來執行刪除操做
const apts = (state=[], action) => {
    const handleDelete = (arr, id) => {
        for(let i=0; i<arr.length; i++) {
            if (arr[i].id === id) {
                return [
                    ...arr.slice(0, i),
                    ...arr.slice(i+1)
                ]
            }
        }
    };

    switch (action.type) {
        // 根據action傳入的數據apt再加上id來生成一個新的預定
        case ADD_APT:
            return [
                ...state,
                Object.assign({}, action.apt, {
                    id: ++id
                })
            ]
        case DELETE_APT:
            return handleDelete(state, action.id);
        default:
            return state;
    }
};

export default apts;

查詢和排列方式

這三個函數的做用就是根據action傳入的數據,更新state裏的對應值,在這裏並不會真正的去處理預定的列表。

reducers/orderBy.js:

import { CHANGE_ORDER_BY } from '../constants';

const orderBy = (state=null, action) => {
    switch (action.type) {
        case CHANGE_ORDER_BY:
            return action.orderBy
        default:
            return state;
    }
};

export default orderBy;

reducers/orderDir.js:

import { CHANGE_ORDER_DIR } from '../constants';

const orderDir = (state=null, action) => {
    switch (action.type) {
        case CHANGE_ORDER_DIR:
            return action.orderDir
        default:
            return state;
    }
};

export default orderDir;

reducers/query.js:

import { QUERY } from '../constants';

const query = (state=null, action) => {
    switch (action.type) {
        case QUERY:
            return action.query;
        default:
            return state;
    }
}

export default query;

合成reducers

reducers/index.js:

import { combineReducers } from 'redux';

import apts from './apts';
import openDialog from './openDialog';
import formExpanded from './formExpanded';
import query from './query';
import orderBy from './orderBy';
import orderDir from './orderDir';

// redux提供的combineReducers函數用來將處理不一樣部分的state的函數合成一個
// 每當action進來的時候會通過每個reducer函數,可是因爲action類型(type)的不一樣
// 只有符合(switch語句的判斷)的reducer纔會處理,其餘的只是將state原封不動返回

const reducers = combineReducers({
    apts,
    openDialog,
    formExpanded,
    query,
    orderBy,
    orderDir
});

export default reducers;

容器組件

containers/AddForm.js:

import { connect } from 'react-redux';

import { addApt, toggleDialog, toggleFormExpanded } from '../actions';

import AddForm from '../components/AddForm';

// AddForm組件可經過props來獲取兩個state:open和formExpanded
const mapStateToProps = (state) => ({
    open: state.openDialog,
    formExpanded: state.formExpanded
});

// 使得AddForm組件能夠經過props獲得三個回調函數,調用便可至關於發起action
const mapDispatchToProps = ({
    toggleFormExpanded,
    toggleDialog,
    handleAdd: newApt => addApt(newApt)
});

// 使用react-redux提供的connect函數,能夠將一個組件提高爲容器組件,容器組件可直接獲取到state、能夠直接使用dispatch。
// 這個connect函數接受兩個函數做爲參數,這兩個做爲參數的函數的返回值都是對象, 按約定他們分別命名爲mapStateToProps,mapDispatchToProps
// mapStateToProps肯定了在這個組件中能夠得到哪些state,這裏的話只用到了兩個UI相關的state:open和formExpanded,這些state均可經過組件的props來獲取
// mapDispatchToProps原本應該是返回對象的函數,這裏比較簡單,直接寫成一個對象,肯定了哪些action是這個組件能夠發起的,也是經過組件的props來獲取
// connect函數的返回值是一個函數,接受一個組件做爲參數。

export default connect(mapStateToProps, mapDispatchToProps)(AddForm);

containers/App.js:

import React from 'react';
import { connect } from 'react-redux';

import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import injectTapEventPlugin from 'react-tap-event-plugin';

injectTapEventPlugin();
import AppBar from 'material-ui/AppBar';
import Paper from 'material-ui/Paper';

import AddForm from '../containers/AddForm';
import Search from '../components/Search';
import Sort from '../components/Sort';
import AptList from '../components/AptList';

import { deleteApt, query, changeOrderBy, changeOrderDir } from '../actions';

const paperStyle = {
  minHeight: 600,
  width: 360,
  margin: '20px auto',
  textAlign: 'center'
};

const App = ({
  apts,
  dispatch,
  orderBy,
  orderDir,
  handleSearch,
  handleDelete,
  handleOrderByChange,
  handleOrderDirChange
}) => (
  <MuiThemeProvider>
    <div>
      <AppBar
        title="React Redux Appointment"
        showMenuIconButton={false}
      />
      <Paper style={paperStyle} zDepth={5}>
        <AddForm />
        <Search handleSearch={handleSearch}/>
        <Sort
          orderBy={orderBy}
          orderDir={orderDir}
          handleOrderByChange={handleOrderByChange}
          handleOrderDirChange={handleOrderDirChange}
        />
        <AptList
          apts={apts}
          handleDelete={handleDelete}
        />
      </Paper>
    </div>
  </MuiThemeProvider>
);


// 處理搜索和排序,返回處理後數組
const handledApts = (apts, query, orderBy, orderDir) => {
    const filterArr = (arr, query) => {
        return arr.filter(item => (
            item.guestName.toLowerCase().indexOf(query) !== -1 ||
            item.date.indexOf(query) !== -1 ||
            item.time.indexOf(query) !== -1 ||
            item.note.toLowerCase().indexOf(query) !== -1)
        );
    };

    const sortArr = (arr, orderBy, orderDir) => {
      if (orderBy && orderDir) {
        return arr.sort((apt1, apt2) => {
          const value1 = apt1[orderBy].toString().toLowerCase();
          const value2 = apt2[orderBy].toString().toLowerCase();
            if (value1 < value2) {
                return orderDir === 'asc' ? -1 : 1;
            } else if (value1 > value2) {
                return orderDir === 'asc' ? 1 : -1;
            } else {
                return 0;
            }
        })
      } else {
        return arr;
      }
    };

    if (!query) {
      return sortArr(apts, orderBy, orderDir);
    } else {
      return sortArr(filterArr(apts, query), orderBy, orderDir);
    }
};


// App組件可經過props來獲取到四個state:query, orderBy, orderDir, apts
// 這裏是真正處理搜索和排序的地方,並非直接將state中的apts返回,而是調用handleApts,返回處理的數組
const mapStateToProps = (state) => ({
    query: state.query,
    orderBy: state.orderBy,
    orderDir: state.orderDir,
    apts: handledApts(state.apts, state.query, state.orderBy, state.orderDir),
});

// App組件可經過props來獲取到四個函數,也就是發起四個action:handleSearch,handleDelete,handleOrderByChange,handleOrderDirChange
const mapDispatchToProps = ({
    handleSearch: searchText => query(searchText),
    handleDelete: id => deleteApt(id),
    handleOrderByChange: orderBy => changeOrderBy(orderBy),
    handleOrderDirChange: orderDir => changeOrderDir(orderDir)
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

入口文件

src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';

import { createStore } from 'redux';
import { Provider } from 'react-redux';

import App from './containers/App';
import './index.css';

import reducers from './reducers';

// 使用createStore表示應用的store,傳入的第一個參數是reducers,第二個參數是Redux的調試工具
const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

// 使用react-redux提供的Provider組件,使App組件及子組件能夠獲得store的相關的東西,如store.getState(),store.dispatch()等。
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

結尾

React提供的是經過state來控制控制UI和單向數據流動,
Redux提供的是單一數據源和只能經過action和reducer來處理state的更新。

以其中的點擊按鈕顯示新建預定表單的過程來捋一捋React、React-Redux的邏輯(靈感來源於自Cory House大神):

  • 用戶:點擊按鈕

  • React:哈嘍,action生成函數toggleFormExpanded,有人點擊了展開新建預定的表單。

  • Action:收到,謝謝React,我立刻發佈一個action也就是{type:TOGGLE_FORM_EXPANDED}告訴reducers來更新state。

  • Reducer:謝謝Action,我收到你的傳過來要執行的action了,我會根據你傳遞進來的{type:TOGGLE_FORM_EXPANDED},先複製一份當前的state,而後把state中的formExpanded的值更新爲true,而後把新的state給Store。

  • Store:嗯,Reducer你幹得漂亮,我收到了新的state,我會通知全部與我鏈接的組件,確保他們會收到新state。

  • React-Redux:啊,感謝Store傳來的新數據,我如今就看看React界面是否須要須要發生變化,啊,須要把新建預定的表單顯示出來啊,那界面仍是要更新一下的,交給你了,React。

  • React:好的,有新的數據由store經過props傳遞下來的數據了,我會立刻根據這個數據把新建預定的表單顯示出來。

  • 用戶:看到了新建預定的表單。

若是以爲還不錯,來個star吧。(笑臉)

相關文章
相關標籤/搜索