---
# System prepended metadata

title: React - 留言板2
tags: [Example]

---

---
tags: Example,
disqus: hackmd
---

# React - 留言板2

基本上是根據[React - 留言板](https://hackmd.io/pAypxgRBTzKBaN7m2X4Xug)來延續下去的。

主要為新增，編輯訊息、刪除訊息、以及將訊息保留畫面重整不遺失的功能。

因為前一張的資料結構關係，在編輯訊息時遇到同樣為送出按鈕，但是要如何克服我是要找到父留言的訊息還是子留言的訊息。

![我的留言資料結構畫面](https://i.imgur.com/QgoAtR1.png)

我的子留言的data是放在父留言的`responseArray`底下的。

因為我在新增子留言的時候直接把子留言的data放到父的`responseArray`，
這樣做導致我在編輯或刪除的時候必須去多傳一個props去判斷現在是父還是子要編輯或刪除。

### 理想改動

父留言的data結構一樣維持本來的樣子，但是子留言在新增時不是直接塞到父留言的`responseArray`，而是跟父留言一樣只是要多一個欄位這個欄位要放父留言的`id`

```javascript=
id: 1001, name: chris, time: '2020-07-10', text: 'hello'

id: 2001, parentId: 1001 name: chris, time: '2020-07-10', text: 'hello'
```

類似這樣的做法，然後再透過處理陣列把有`parentId`的留言放到對應的父留言的`responseArray`裡。

每次新增或刪除，即可以找到對應的父id然後重新把剩下對應到的子留言放入。

---

附上我目前在留言板2的更動

Home.js

```javascript=
import React from 'react';
import { connect } from 'react-redux';
import { addMessage } from '../redux/action/message';
import styled from 'styled-components';
import InputBox from '../component/InputBox';
import MessageContainer from '../component/MessageContainer';

class Home extends React.Component {
    constructor(props) {
        super(props);
        this.props = props;

        this.handleSumbitMessage = this.handleSumbitMessage.bind(this);
    }

    handleSumbitMessage(e) {
        const { addMessage, messageList } = this.props;
        addMessage([...messageList, {
            id: Date.now(),
            ...e,
        }]);
    }

    componentDidMount() {
        const { addMessage } = this.props;
        const messageList = JSON.parse(localStorage.getItem('messageData'));

        messageList && addMessage(messageList);
    }

    render(){
        const {messageList} = this.props;
        return(
          <HomeWrap>
            <Title> React 留言板 </Title>
            <InputBox onSubmitMessage={this.handleSumbitMessage} />
            <MessageContainer messageList={messageList} />
          </HomeWrap>
        );
    }
}

const Title = styled.h1`
    margin: 0 auto 15px;
    color: #0CA0F9;
    text-align: center;
`;

const HomeWrap = styled.div`
    background: #BEDADB;
    margin: 0 auto;
    padding: 15px;
    width: 700px;
`;

const mapStateToProps = (state) => ({
    messageList: state.messageList,
});

const mapDispatchToProps = (dispatch) => ({
    addMessage: (e) => { dispatch(addMessage(e)); },
});

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(Home);
```

reducer -> message.js

```javascript=
import { combineReducers } from 'redux';
import {ADD_MESSAGE} from '../action/message';

const defaultMessage = {
    id: 0,
    name: 'chris',
    text: '測試一號',
    time: '1990-07-8 12:00:00',
    responseArray: [],
}

function messageList(state = [defaultMessage], action) {
    switch (action.type) {
        case ADD_MESSAGE:
            localStorage.setItem('messageData', JSON.stringify(action.info));
            return action.info;
        default:
            return state;
    }
}

const rootReducer = combineReducers({
    messageList,
});
export default rootReducer;
```

InputBox.js

```javascript=
import React from 'react';
import styled from 'styled-components';

export default class InputBox extends React.Component {
    constructor(props) {
        super(props);
        this.props = props;
        this.state = {
            name: '',
            text: '',
        };

        this.handleTextChange = this.handleTextChange.bind(this);
        this.handleNameChange = this.handleNameChange.bind(this);
        this.handleGetTime = this.handleGetTime.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleTextChange(e) {
        this.setState({text: e.currentTarget.value});
    }

    handleNameChange(e) {
        this.setState({name: e.currentTarget.value});
    }

    handleGetTime(fmt) {
        const date = new Date();
        const o = {
            "M+": date.getMonth() + 1, //月份
            "d+": date.getDate(), //日
            "h+": date.getHours(), //小時
            "m+": date.getMinutes(), //分
            "s+": date.getSeconds(), //秒
            "q+": Math.floor((date.getMonth() + 3) / 3), //季度
            "S": date.getMilliseconds() //毫秒
        };
        if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
        for (var k in o)
        if (new RegExp("(" +  k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" +  o[k]).substr(("" + o[k]).length)));
        return fmt;
    }

    handleSubmit() {
        const {isShowInputBox, onSubmitMessage, isReset, isChild, id} = this.props;
        const {name, text} = this.state;

        if (!text) {
            return;
        }

        onSubmitMessage({
            name: name || '匿名人士',
            text,
            time: this.handleGetTime('yyyy-MM-dd hh:mm:ss'),
            responseArray: [],
        }, isChild, id);

        // isReset，按下送出後需要將name跟text清空，除了編輯訊息
        if (isReset) {
            this.setState({
                name: '',
                text: ''
            });
        }

        // 控制編輯訊息的輸入框狀態
        isShowInputBox && isShowInputBox();
    }

    componentDidMount() {
        this.setState({
            name: this.props.propsName || '',
            text: this.props.propsValue || '',
        });
    }

    render() {
        const {propsName} = this.props;
        const {name, text} = this.state;
        return (
            <InputBoxWrap>
                <Textarea placeholder="在這留下你想說的話" value={text} onChange={this.handleTextChange} />
                <Nickname type="text" placeholder="您的大名" value={name} onChange={this.handleNameChange} disabled={propsName} />
                <Submit onClick={this.handleSubmit}>確認送出</Submit>
            </InputBoxWrap>
        );
    }
}

InputBox.defaultProps = {
    isReset: true,
    isChild: false,
};

const Submit = styled.button`
    border: none;
    border-radius: 5px; 
    height: 30px;
    cursor: pointer;
    transition: all .5s ease;
    user-select: none;

    &:focus {
        outline: none;
    }
    &:hover {
        background: #FEBFCF;
    }
`;

const Nickname = styled.input`
    margin-right: 20px;
    border: none;
    border-radius: 5px;
    padding: 0 10px;
    width: 150px;
    height: 30px;
    box-sizing: border-box;

    &:focus {
        outline: none;
    }
`;

const Textarea = styled.textarea`
    border: none;
    border-radius: 5px;
    padding: 10px;
    width: 100%;
    height: 200px;
    font-size: 20px;
    resize: none;
    box-sizing: border-box;

    &:focus {
        outline: none;
    }
`;

const InputBoxWrap = styled.div`
    margin: 0 auto 20px;
    width: 500px;
    max-width: 100%;
`;
```

MessageContainer.js

```javascript=
import React from 'react';
import styled from 'styled-components';
import MessageItem from '../component/MessageItem';

export default class MessageContainer extends React.Component {
    constructor(props) {
        super(props);
        this.props = props;
    }

    render() {
        const {messageList} = this.props;

        return (
            <MessageWrap>
                {
                    messageList.map((element, index) => {
                        return <MessageItem 
                            key={index}
                            id={element.id}
                            name={element.name} 
                            value={element.text} 
                            time={element.time}
                            responseArray={element.responseArray}
                        />
                    })
                }
            </MessageWrap>
        );
    }
}

const MessageWrap = styled.div`
    margin: 0 auto;
    padding: 10px;
    width: 500px;
    min-height: 300px;
`;
```

MessageItem.js

```javascript=
import React from 'react';
import { connect } from 'react-redux';
import { addMessage } from '../redux/action/message';
import styled from 'styled-components';
import InputBox from '../component/InputBox';
import MessageAtom from '../component/MessageAtom';

class MessageItem extends React.Component {
    constructor(props) {
        super(props);
        this.props = props;
        this.state = {
            isShowResponse: false,
        }

        this.handleSumbitMessage = this.handleSumbitMessage.bind(this);
        this.handleToggleResponse = this.handleToggleResponse.bind(this);
        this.handleEditMessage = this.handleEditMessage.bind(this);
    }

    handleSumbitMessage(e) {
        const { addMessage, messageList, id } = this.props;

        const cloneMessageList = Object.assign([], messageList);

        cloneMessageList.find((element) => {
            if (id === element.id) {
                element.responseArray = [
                    ...element.responseArray,
                    {
                        id: Date.now(),
                        name: e.name,
                        text: e.text,
                        time: e.time,
                    }
                ]
            }
        });

        addMessage(cloneMessageList);
    }

    handleToggleResponse() {
        const {isShowResponse} = this.state;
        this.setState({isShowResponse: !isShowResponse});
    }

    handleEditMessage(e, isChild, id) {
        const { addMessage, messageList } = this.props;

        const cloneMessageList = Object.assign([], messageList);

        if (isChild) {
            cloneMessageList.map(list => {
                list.responseArray.find(childList => {
                    if (id === childList.id) {
                        childList.text = e.text;
                        childList.time = e.time;
                    }
                });
            });
        } else {
            cloneMessageList.find(list => {
                if (id === list.id) {
                    list.text = e.text;
                    list.time = e.time;
                }
            });
        }

        addMessage(cloneMessageList);
    }
    
    render(){
        const {responseArray, name, time, value, id} = this.props
        const {isShowResponse} = this.state;
        return (
            <MessageItemWrap>
                <MessageAtom id={id} name={name} time={time} value={value} onSubmitMessage={this.handleEditMessage} />

                {
                    responseArray && responseArray.map((element, index) => {
                        return (
                            <ResponseMessage key={index}>
                                <MessageAtom id={element.id} name={element.name} time={element.time} value={element.text} onSubmitMessage={this.handleEditMessage} isChild={true} />
                            </ResponseMessage>
                        );
                    })
                }
    
                <ResponseButton isShow={isShowResponse} onClick={this.handleToggleResponse}>發表回應</ResponseButton>

                {
                    isShowResponse && <InputBox isShowInputBox={this.handleToggleResponse} onSubmitMessage={this.handleSumbitMessage} />
                }
            </MessageItemWrap>
        );
    }
}

const mapStateToProps = (state) => ({
    messageList: state.messageList,
});

const mapDispatchToProps = (dispatch) => ({
    addMessage: (e) => { dispatch(addMessage(e)); },
});

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

const ResponseButton = styled.div`
    display: inline-block;
    margin-bottom: ${props => props.isShow && '15px'};
    max-width: 80px;
    height: 30px;
    cursor: pointer;
    color: #00A0E9;
    font-size: 14px;
    line-height: 30px;
    user-select: none;

    &:hover {
        color: #37C0FF;
    }
`;

const ResponseMessage = styled.div`
    margin: 5px 0 0 40px;
`;

const MessageItemWrap = styled.div`
    margin-bottom: 15px;
    padding: 15px;
    border-radius: 5px;
    background: #E6E6E6;
    width: 500px;
    box-sizing: border-box;

    p {
        margin: 0;
    }
`;
```

MessageAtom.js

```javascript=
import React from 'react';
import { connect } from 'react-redux';
import { addMessage } from '../redux/action/message';
import styled from 'styled-components';
import InputBox from '../component/InputBox';

class MessageAtom extends React.Component {
    constructor(props) {
        super(props);
        this.props = props;
        this.state = {
            isShowInputBox: false,
        }

        this.handleShowInputBox = this.handleShowInputBox.bind(this);
        this.handleDeleteMessage = this.handleDeleteMessage.bind(this);
    }

    handleShowInputBox() {
        const {isShowInputBox} = this.state;
        this.setState({isShowInputBox: !isShowInputBox});
    }

    handleDeleteMessage() {
        const {addMessage, messageList, id, isChild} = this.props;

        const cloneMessageList = Object.assign([], messageList);

        if (isChild) {
            cloneMessageList.map((list, index) => {
                list.responseArray.find((childList, childIndex) => {
                    if (id === childList.id) {
                        cloneMessageList[index].responseArray.splice(childIndex, 1);

                        addMessage(cloneMessageList);

                        return childList;
                    }
                });
            });
        } else {
            cloneMessageList.find((list, index) => {
                if (id === list.id) {
                    cloneMessageList.splice(index, 1);

                    addMessage(cloneMessageList);

                    return list;
                }
            });
        }
    }

    render() {
        const {onSubmitMessage, id, name, time, value, isChild} = this.props;
        const {isShowInputBox} = this.state;
        return (
            <>
                <Info><span>{name}</span> 在 {time} 發佈了這則訊息</Info>
                <Message>{value}</Message>
                <HandleWrap>
                    <HandleButton onClick={this.handleShowInputBox}>編輯訊息</HandleButton>
                    <HandleButton onClick={this.handleDeleteMessage}>刪除訊息</HandleButton>
                </HandleWrap>
                {
                    isShowInputBox && <InputBox
                        isShowInputBox={this.handleShowInputBox}
                        onSubmitMessage={onSubmitMessage} 
                        id={id}
                        propsName={name} 
                        propsValue={value}
                        isChild={isChild}
                        isReset={false}
                    />
                }
            </>
        );
    }
}

const mapStateToProps = (state) => ({
    messageList: state.messageList,
});

const mapDispatchToProps = (dispatch) => ({
    addMessage: (e) => { dispatch(addMessage(e)); },
});

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

const HandleButton = styled.div`
    margin-left: 10px;
    display: inline-block;
    max-width: 80px;
    height: 30px;
    cursor: pointer;
    color: #00A0E9;
    font-size: 14px;
    line-height: 30px;
    user-select: none;

    &:hover {
        color: #E90000;
    }
`;

const HandleWrap = styled.div`
    text-align: right;
`;

const Message = styled.pre`
    margin: 0;
    padding: 10px 0;
    color: #303233;
    font-size: 16px;
`;

const Info = styled.p`
    color: #666;
    font-size: 14px;

    span {
        color: #00A0E9;
        font-weight: bold;
    }
`;
```

InputBox.js

```javascript=
import React from 'react';
import styled from 'styled-components';

export default class InputBox extends React.Component {
    constructor(props) {
        super(props);
        this.props = props;
        this.state = {
            name: '',
            text: '',
        };

        this.handleTextChange = this.handleTextChange.bind(this);
        this.handleNameChange = this.handleNameChange.bind(this);
        this.handleGetTime = this.handleGetTime.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleTextChange(e) {
        this.setState({text: e.currentTarget.value});
    }

    handleNameChange(e) {
        this.setState({name: e.currentTarget.value});
    }

    handleGetTime(fmt) {
        const date = new Date();
        const o = {
            "M+": date.getMonth() + 1, //月份
            "d+": date.getDate(), //日
            "h+": date.getHours(), //小時
            "m+": date.getMinutes(), //分
            "s+": date.getSeconds(), //秒
            "q+": Math.floor((date.getMonth() + 3) / 3), //季度
            "S": date.getMilliseconds() //毫秒
        };
        if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
        for (var k in o)
        if (new RegExp("(" +  k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" +  o[k]).substr(("" + o[k]).length)));
        return fmt;
    }

    handleSubmit() {
        const {isShowInputBox, onSubmitMessage, isReset, isChild, id} = this.props;
        const {name, text} = this.state;

        if (!text) {
            return;
        }

        onSubmitMessage({
            name: name || '匿名人士',
            text,
            time: this.handleGetTime('yyyy-MM-dd hh:mm:ss'),
            responseArray: [],
        }, isChild, id);

        // isReset，按下送出後需要將name跟text清空，除了編輯訊息
        if (isReset) {
            this.setState({
                name: '',
                text: ''
            });
        }

        // 控制編輯訊息的輸入框狀態
        isShowInputBox && isShowInputBox();
    }

    componentDidMount() {
        this.setState({
            name: this.props.propsName || '',
            text: this.props.propsValue || '',
        });
    }

    render() {
        const {propsName} = this.props;
        const {name, text} = this.state;
        return (
            <InputBoxWrap>
                <Textarea placeholder="在這留下你想說的話" value={text} onChange={this.handleTextChange} />
                <Nickname type="text" placeholder="您的大名" value={name} onChange={this.handleNameChange} disabled={propsName} />
                <Submit onClick={this.handleSubmit}>確認送出</Submit>
            </InputBoxWrap>
        );
    }
}

InputBox.defaultProps = {
    isReset: true,
    isChild: false,
};

const Submit = styled.button`
    border: none;
    border-radius: 5px; 
    height: 30px;
    cursor: pointer;
    transition: all .5s ease;
    user-select: none;

    &:focus {
        outline: none;
    }
    &:hover {
        background: #FEBFCF;
    }
`;

const Nickname = styled.input`
    margin-right: 20px;
    border: none;
    border-radius: 5px;
    padding: 0 10px;
    width: 150px;
    height: 30px;
    box-sizing: border-box;

    &:focus {
        outline: none;
    }
`;

const Textarea = styled.textarea`
    border: none;
    border-radius: 5px;
    padding: 10px;
    width: 100%;
    height: 200px;
    font-size: 20px;
    resize: none;
    box-sizing: border-box;

    &:focus {
        outline: none;
    }
`;

const InputBoxWrap = styled.div`
    margin: 0 auto 20px;
    width: 500px;
    max-width: 100%;
`;
```