Building a Personal Blog from Zero (4) - Message Comment Area

Keywords: React emoji github

Code: https://github.com/MSChuan/Blog_Comment
Demo: https://mschuan.github.io/Blog_Comment/dist_prod/index.html

This article will implement a simple comment area, which can be divided into two types. For example, the parent comment called "test" and "sofa" are used here, and "your two goods" is a response to "sofa", which is called child comment here. The top part is the area where parent comments are added. Each message has a reply and compliment button, which is used to generate child comments and compliments in response. The corresponding number needs to be + 1 after clicking.

State

Because react+redux maintains a global state tree, we need to define it first before we can continue to implement subsequent coding. First of all, of course, the content of the comments is stored. Every parent comment + related child comment is regarded as a package, so we can use an array like packages to store the whole content. Each package is also an array. Each element is an object, including the content of the comments, the number of replies, the number of points in praise, the id, the creation time and so on. After clicking on the reply, you need to pop up the reply box. The location where the reply box appears also needs to be defined in the state.

const initialState = {
    CommentState: {
        packages: [],
        replyBoxIndex: {
            packageIndex: -1,
            commentIndex: -1
        }
    }
};

The initial state is defined here. The location of the reply box requires two indexes, one is the index of the package, the other is the reply box below which comment in the package appears. The initial - 1 indicates that there is no reply box.

Action

Functionally, there are only four actions: parent comment, child comment, click the reply button and click on praise. So we define these four actions. First, we create a new type. JS file to define all actions.

const types = {
    AddParentComment: 'AddParentComment',
    AddReplyBox: 'AddReplyBox',
    PraiseComment: 'PraiseComment',
    AddChildComment: 'AddChildComment',
};

export default types;

Define actionFactory to generate all actions.

import types from './Types';

const actionFactory = {
    AddParentComment: (text, articleId) => ({
        type: types.AddParentComment,
        text: text,
        articleId: articleId
    }),
    AddReplyBox: (packageIndex, commentIndex) => ({
        type: types.AddReplyBox,
        packageIndex: packageIndex,
        commentIndex: commentIndex
    }),
    PraiseComment: (packageIndex, commentIndex, id) => ({
        type: types.PraiseComment,
        packageIndex: packageIndex,
        commentIndex: commentIndex,
        id: id
    }),
    AddChildComment: (packageIndex, id, text) => ({
        type: types.AddChildComment,
        packageIndex: packageIndex,
        text: text,
        id: id
    }),
};

export default actionFactory;

AddParentComment requires two parameters, one is the content, the other is the article id. When the article id is 0, we think it's a message on the message board. The other action parameters are not explained one by one, and are relatively simple.

Reducer

With state and action, you can write reducer clearly.

const packages = (state = initialState.CommentState.packages, action) => {
    switch(action.type) {
        case types.AddChildComment:
            if(action.id !== state[action.packageIndex][0].id) {
                return state;
            }
            return ([
                ...state.slice(0, action.packageIndex),
                [Object.assign({},state[action.packageIndex][0],{replyCount: state[action.packageIndex][0].replyCount + 1}),
                    ...state[action.packageIndex].slice(1), {text: action.text, created_at: '', praiseCount: 0, replyCount: 0, id: 0 }],
                ...state.slice(action.packageIndex + 1)
            ]);
        case types.AddParentComment:
            return ([[{text: action.text, created_at: '', praiseCount: 0, replyCount: 0, id: 0}], ...state]);
        case types.PraiseComment:
            return ([
                ...state.slice(0, action.packageIndex),
                [...state[action.packageIndex].slice(0, action.commentIndex),
                    Object.assign({}, state[action.packageIndex][action.commentIndex], {praiseCount: state[action.packageIndex][action.commentIndex].praiseCount + 1}),
                    ...state[action.packageIndex].slice(action.commentIndex + 1)],
                ...state.slice(action.packageIndex + 1)
            ]);
        default:
            return state;
    }
};

First of all, packages, which is ugly here, is a better way to encapsulate those very long and complex logic into functions, which can be reused, and also make the switch case clearer.

const replyBoxIndex = (state = initialState.CommentState.replyBoxIndex, action) => {
    switch(action.type) {
        case types.AddReplyBox:
            if(state.packageIndex === action.packageIndex && state.commentIndex === action.commentIndex) {
                return initialState.CommentState.replyBoxIndex;
            }
            return ({
                packageIndex: action.packageIndex,
                commentIndex: action.commentIndex
            });
        default:
            return state;
    }
};

For the reply box, toggle is implemented here, that is, clicking the same reply button can switch the corresponding reply box.

Realization of React

The whole structure is also clear, with a Container to wrap up the comment area, the following definition of two Component s, one is to add parent comment, one shows the contents of the packages.

Container

class CommentContainer extends React.Component {
    constructor(props) {
        super(props);

        // 0 means this comment container is not related to any article
        this.articalId = props.articalId || 0;
    }
    componentDidMount() {
        // TODO: send request to fetch comments content
    }
    render() {
        const { state, actions } = this.props;
        let commentPackageList = state.packages.map((p, index) => {
            return (<div className="commentPackageBox" >
                        <CommentList
                            comments={p}
                            packageIndex={index}
                            replyBoxIndex={(index === state.replyBoxIndex.packageIndex) ? state.replyBoxIndex.commentIndex : -1}
                            actions={actions}
                        />
                    </div>);
        });

        return (<div>
                    <AddParentComment actions={actions} articalId={this.articalId} />
                    {commentPackageList}
                </div>);
    }
}

Get the state, action from the store, and pass it to the subcomponents after processing. The map function is especially useful when dealing with arrays.

Component

class AddParentComment extends React.Component {
    constructor(props) {
        super(props);
        this.editor = null;
    }
    componentDidMount() {
        const textbox = ReactDOM.findDOMNode(this.refs.AddParentCommentBox);
        this.editor = new Simditor({
            textarea: $(textbox),
            toolbar: ['title', 'bold', 'italic', 'underline', 'strikethrough', 'color', 'ol', 'ul', 'link', 'alignment', 'emoji'],
            emoji: {
                imagePath: config.emojiUrl
            }
        });
    }
    render() {
        const {actions, articalId} = this.props;
        return (<form>
                    <FormGroup controlId="AddParentCommentBox">
                        <FormControl componentClass="textarea" placeholder="Leaving a message." ref={'AddParentCommentBox'} />
                        <Button type="submit" onClick={(e) => {
                            e.preventDefault();
                            if(!!this.editor && this.editor.getValue() !== '') {
                                actions.AddParentComment(this.editor.getValue(), articalId);
                                this.editor.setValue('');
                            }
                        }}>Submission</Button>
                    </FormGroup>
                </form>);
    }
}

The content of the component is an edit box + submit button, which can be easily implemented using react-bootstrap. It's a little more complicated to display the comment content. First, parent comment and child comment are generated according to packages render. If you need to display the reply box, insert the reply box in the corresponding position. Better to reuse the component of AddParentComment, because the reply box is actually isomorphic to AddParentComment.

class CommentList extends React.Component {
    constructor(props) {
        super(props);
        this.editor = null;
    }
    componentDidUpdate() {
        if(!!this.refs.commentReplyBox) {
            const textbox = ReactDOM.findDOMNode(this.refs.commentReplyBox);
            this.editor = new Simditor({
                textarea: textbox,
                toolbar: ['title', 'bold', 'italic', 'underline', 'strikethrough', 'color', 'ol', 'ul', 'link', 'alignment', 'emoji'],
                emoji: {
                    imagePath: config.emojiUrl
                }
            });
        }
    }
    render() {
        const {comments, packageIndex, replyBoxIndex, actions} = this.props;
        let list = comments.map((comment, index) =>{
            return (
                <ListGroupItem className="commentOutline">
                    <div className="commentContent" dangerouslySetInnerHTML={{__html: comment.text}}></div>
                    <Form horizontal>
                        <FormGroup>
                            <ControlLabel>{comment.created_at}</ControlLabel>
                            <Button bsStyle="link" eventKey={3} href="#" onClick={(e) => {
                                e.preventDefault();
                                actions.AddReplyBox(packageIndex, index);
                            }}>{'Reply ' + (index === 0 ? comment.replyCount : '')}</Button>
                            <Button bsStyle="link" eventKey={2} href="#" onClick={(e) => {
                                e.preventDefault();
                                actions.PraiseComment(packageIndex, index, comments[index].id);
                            }} >{'Fabulous ' + comment.praiseCount}</Button>
                        </FormGroup>
                    </Form>
                </ListGroupItem>
            );
        });

        if(replyBoxIndex >= 0) {
            list.splice(replyBoxIndex + 1, 0,
                <ListGroupItem id="commentReplyOutline">
                    <FormGroup controlId="commentReplyBox">
                        <FormControl componentClass="textarea" placeholder="Reply" ref={'commentReplyBox'} />
                        <Button className="commentReplyBoxReplyButton" type="submit" onClick={(e) => {
                            e.preventDefault();
                            if(!!this.editor && this.editor.getValue() !== '') {
                                actions.AddChildComment(packageIndex, comments[0].id, this.editor.getValue());
                                actions.AddReplyBox(-1, -1);
                            }
                        }}>Reply</Button>
                    </FormGroup>
                </ListGroupItem>
            );
        }

        return <ListGroup className="commentList">{list}</ListGroup>;
    }
}

Before writing the code, the packaging of components is not well considered, and there is no maximum reuse of components. Because of the time relationship, mark it first.

Finally, add css to allow child comment to display left margin while adjusting the height of the edit box.

.commentList {
  & > li:not(:first-child) {
      margin-left: 10%;
  }
}

.simditor-body {
    min-height: 100px !important;
}

Posted by Clinger on Wed, 19 Jun 2019 14:41:38 -0700