owned this note
owned this note
Published
Linked with GitHub
---
tags: Example,
disqus: hackmd
---
# React - 留言板
使用React製作一個簡易的留言板,搭配Redux更新畫面留言內容,內容並未寫入至資料庫,所以畫面重整留言就會沒了。
[codesandbox線上畫面版](https://codesandbox.io/s/currying-haze-zsqsz),因為是線上版的所以有些檔案會不在下面這張圖上面。
![](https://i.imgur.com/3GiPRt5.png)
public -> index.html
```htmlmixed=
<!-- sourced from https://raw.githubusercontent.com/reactjs/reactjs.org/master/static/html/single-file-example.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>React留言板</title>
</head>
<body>
<div id="root"></div>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script src="../dist/bundle.js"></script>
</body>
</html>
```
package.json
```json=
{
"name": "react-message-board",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --mode development"
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hot-loader": "^4.12.21",
"react-redux": "^7.2.0",
"redux": "^4.0.5",
"styled-components": "^5.1.1"
},
"devDependencies": {
"@babel/cli": "^7.10.4",
"@babel/core": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"babel-plugin-styled-components": "^1.10.7",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}
```
webpack.config.js
```javascript=
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src/index.js",
mode: "development",
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
loader: "babel-loader",
options: { presets: ["@babel/env"] }
}
]
},
resolve: { extensions: ["*", ".js", ".jsx"] },
output: {
path: path.resolve(__dirname, "dist/"),
publicPath: "/dist/",
filename: "bundle.js"
},
devServer: {
contentBase: path.join(__dirname, "public/"),
port: 3000,
publicPath: "http://localhost:3000/dist/",
hotOnly: true
},
plugins: [new webpack.HotModuleReplacementPlugin()]
};
```
.babelrc
```javascript=
{
"presets": ["@babel/env", "@babel/preset-react"],
"plugins": [
[
"babel-plugin-styled-components",
{
"displayName": true
}
]
]
}
```
app.js
```javascript=
import React, { Component} from 'react';
import { hot } from 'react-hot-loader/root';
import { Provider } from 'react-redux'
import { createStore } from 'redux';
import rootReducer from './redux/reducer/message';
import Home from './page/Home';
const store = createStore(rootReducer);
class App extends Component{
render(){
return(
<Provider store={store}>
<div className="App">
<Home />
</div>
</Provider>
);
}
}
export default hot(App);
```
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, e]);
}
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);
```
action -> message.js
```javascript=
/*
* action type
*/
export const ADD_MESSAGE = 'ADD_MESSAGE';
/*
* action creator
*/
export function addMessage(info) {
return { type: ADD_MESSAGE, info };
}
```
reducer -> message.js
```javascript=
import { combineReducers } from 'redux';
import {ADD_MESSAGE} from '../action/message';
const defaultMessage = {
name: 'chris',
text: '測試一號',
time: '1990-07-8 12:00:00',
responseArray: [],
}
function messageList(state = [defaultMessage], action) {
switch (action.type) {
case ADD_MESSAGE:
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 {onSubmitMessage} = this.props;
const {name, text} = this.state;
if (!text) {
return;
}
onSubmitMessage({
name: name || '匿名人士',
text,
time: this.handleGetTime('yyyy-MM-dd hh:mm:ss'),
responseArray: [],
});
this.setState({
name: '',
text: ''
});
}
render() {
const {name, text} = this.state;
return (
<InputBoxWrap>
<Textarea placeholder="在這留下你想說的話" value={text} onChange={this.handleTextChange} />
<Nickname type="text" placeholder="您的大名" value={name} onChange={this.handleNameChange} />
<Submit onClick={this.handleSubmit}>確認送出</Submit>
</InputBoxWrap>
);
}
}
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={index}
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';
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);
}
handleSumbitMessage(e) {
const { addMessage, messageList, id } = this.props;
const cloneMessageList = Object.assign([], messageList);
cloneMessageList.find((element, index) => {
if (id === index) {
element.responseArray = [
...element.responseArray,
{
name: e.name,
text: e.text,
time: e.time,
}
]
}
});
addMessage(cloneMessageList);
}
handleToggleResponse() {
const {isShowResponse} = this.state;
this.setState({isShowResponse: !isShowResponse});
}
render(){
const {name, time, value, responseArray} = this.props
const {isShowResponse} = this.state;
return (
<MessageItemWrap>
<Info><span>{name}</span> 在 {time} 發佈了這則訊息</Info>
<Message>{value}</Message>
{
responseArray && responseArray.map((element, index) => {
return (
<ResponseMessage key={index}>
<Info><span>{element.name}</span> 在 {element.time} 回覆了這則訊息</Info>
<Message>{element.text}</Message>
</ResponseMessage>
);
})
}
<ResponseButton isShow={isShowResponse} onClick={this.handleToggleResponse}>發表回應</ResponseButton>
{
isShowResponse && <InputBox 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 Info = styled.p`
color: #666;
font-size: 14px;
span {
color: #00A0E9;
font-weight: bold;
}
`;
const Message = styled.pre`
margin: 0;
padding: 10px 0;
color: #303233;
font-size: 16px;
`;
const MessageItemWrap = styled.div`
margin-bottom: 15px;
padding: 15px;
border-radius: 5px;
background: #E6E6E6;
width: 500px;
box-sizing: border-box;
p {
margin: 0;
}
`;
```