
In this tutorial of the series ‘creating blog with Laravel and Reactjs’ we continue by processing posts crud with redux, then connecting this to the UI.
As we did with tags and categories in previous lessons we will use the same approach when processing posts. So let’s start with creating the apis.
Create resources/js/admin/apis/Post.js with the code below:
import axios from 'axios'; const Post = { list: (page = 1) => { return axios.get('/posts?page=' + page); }, add: (payload) => { let data = Post.toFormData(payload); return axios.post('/posts', data, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token"), 'Content-Type': 'multipart/form-data'}}); }, showOne: (id) => { return axios.get('/posts/' + id); }, edit: (payload, id) => { let data = Post.toFormData(payload); data.append('_method', 'PUT'); return axios.post('/posts/' + id, data, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token"), 'Content-Type': 'multipart/form-data'}}); }, remove: (id) => { return axios.delete('/posts/' + id, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token")}}); }, toFormData: (payload) => { const formData = new FormData(); for (let key in payload) { if(key != 'tags') { formData.append(key, payload[key]); } else { for(let i=0; i<payload[key].length; i++) { formData.append('tags[]', payload[key][i]); } } } return formData; } }; export default Post;
This is the basic CRUD apis for posts. Here in the add() and edit() i modified the code to use javascript FormData(), that’s because we have file upload and tags[] and also i specified Content-type header of ‘multipart/form-data’.
Manipulating Redux Store
Open resources/js/admin/store/actionTypes/PostTypes.js and update it with this code:
export const LIST_POSTS = 'LIST_POSTS'; export const LIST_POSTS_SUCCESS = 'LIST_POSTS_SUCCESS'; export const LIST_POSTS_FAILURE = 'LIST_POSTS_FAILURE'; export const CREATE_POSTS = 'CREATE_POSTS'; export const CREATE_POSTS_SUCCESS = 'CREATE_POSTS_SUCCESS'; export const CREATE_POSTS_FAILURE = 'CREATE_POSTS_FAILURE'; export const EDIT_POSTS = 'EDIT_POSTS'; export const EDIT_POSTS_SUCCESS = 'EDIT_POSTS_SUCCESS'; export const EDIT_POSTS_FAILURE = 'EDIT_POSTS_FAILURE'; export const DELETE_POSTS = 'DELETE_POSTS'; export const DELETE_POSTS_SUCCESS = 'DELETE_POSTS_SUCCESS'; export const DELETE_POSTS_FAILURE = 'DELETE_POSTS_FAILURE'; export const SHOW_POST = 'SHOW_POST'; export const SHOW_POST_SUCCESS = 'SHOW_POST_SUCCESS'; export const SHOW_POST_FAILURE = 'SHOW_POST_FAILURE'; export const HANDLE_FIELD_CHANGE = 'HANDLE_FIELD_CHANGE'; export const SET_POST_DEFAULTS = 'SET_POST_DEFAULTS'; export const RESET_FIELDS = 'RESET_FIELDS';
This is action types we will need to manipulate redux for posts. Now we will create the actions for posts so open resources/js/admin/store/actions/PostActions.js and add this code:
import * as PostTypes from '../actionTypes/PostTypes'; import Post from '../../apis/Post'; /** * list posts action * @param page */ function listPosts(page) { return function (dispatch, getState) { // start sending request (first dispatch) dispatch({ type: PostTypes.LIST_POSTS }); // async call must dispatch action whether on success or failure Post.list(page).then(response => { dispatch({ type: PostTypes.LIST_POSTS_SUCCESS, data: response.data.data }); }).catch(error => { dispatch({ type: PostTypes.LIST_POSTS_FAILURE, error: error.response.data }); }); } } /** * handle field change * * fires on any field change of the post object */ function handleFieldChange(field, value, checked) { return function (dispatch, getState) { dispatch({ type: PostTypes.HANDLE_FIELD_CHANGE, data: value, field, checked }); } } /** * add post action * * * @returns {Function} */ function addPost(payload, cb) { return function (dispatch, getState) { dispatch({ type: PostTypes.CREATE_POSTS }); Post.add(payload).then(response => { dispatch({ type: PostTypes.CREATE_POSTS_SUCCESS, data: response.data }); cb(); }).catch(error => { dispatch({ type: PostTypes.CREATE_POSTS_FAILURE, error: error.response.data }) }) } } /** * show post action * * @param id * @returns {Function} */ function showPost(id) { return function (dispatch, getState) { dispatch({ type: PostTypes.SHOW_POST }); Post.showOne(id).then(response => { dispatch({ type: PostTypes.SHOW_POST_SUCCESS, data: response.data.data }); }).catch(error => { dispatch({ type: PostTypes.SHOW_POST_FAILURE, error: error.response.data }); }); } } /** * edit post action */ function editPost(id, payload, cb) { return function (dispatch, getState) { dispatch({ type: PostTypes.EDIT_POSTS }); Post.edit(payload, id).then(response => { dispatch({ type: PostTypes.EDIT_POSTS_SUCCESS, data: response.data }); cb(); }).catch(error => { dispatch({ type: PostTypes.EDIT_POSTS_FAILURE, error: error.response.data }) }); } } /** * delete post action */ function deletePost(id) { return function (dispatch, getState) { dispatch({ type: PostTypes.DELETE_POSTS }); Post.remove(id).then(response => { dispatch({ type: PostTypes.DELETE_POSTS_SUCCESS, message: response.data.message, id: id }); }).catch(error => { dispatch({ type: PostTypes.DELETE_POSTS_FAILURE, error: error.response.data }) }); } } function setPostDefaults() { return function (dispatch, getState) { dispatch({ type: PostTypes.SET_POST_DEFAULTS }); } } function resetFields() { return function (dispatch, getState) { dispatch({ type: PostTypes.RESET_FIELDS }); } } export { listPosts, addPost, handleFieldChange, setPostDefaults, resetFields, showPost, editPost, deletePost }
I won’t go through all these actions as they are the same like categories except that handleFieldChange() and resetFields(). handleFieldChange() triggered when any field in the post form changed. resetFields() triggered after and add or update operation to reset fields to default values.
Open resources/js/admin/store/reducers/PostReducer.js and update it with this code:
import * as PostTypes from '../actionTypes/PostTypes'; const initialState = { posts: {}, post: { id: "", title: "", slug: "", content: "", image: "", published: 1, category_id: "", tags: [] }, success_message: "", error_message: "", validation_errors: {}, list_spinner: false, create_update_spinner: false }; const postReducer = function (state = initialState, action) { let tags = []; switch (action.type) { case PostTypes.LIST_POSTS: return { ...state, list_spinner: true }; case PostTypes.LIST_POSTS_SUCCESS: return { ...state, list_spinner: false, posts: action.data }; case PostTypes.LIST_POSTS_FAILURE: return { ...state, list_spinner: false, error_message: action.error }; case PostTypes.HANDLE_FIELD_CHANGE: return handleFieldChange(state, action); case PostTypes.CREATE_POSTS: return { ...state, create_update_spinner: true }; case PostTypes.CREATE_POSTS_SUCCESS: tags = action.data.data.tags; if(tags) { tags = tags.map(x => x['id']); } else { tags = []; } action.data.data.tags = tags; return { ...state, create_update_spinner: false, post: action.data.data, success_message: action.data.message, error_message: "", validation_errors: {} }; case PostTypes.CREATE_POSTS_FAILURE: return { ...state, create_update_spinner: false, error_message: action.error.message, validation_errors: action.error.errors, success_message: "" }; case PostTypes.SHOW_POST: return { ...state, create_update_spinner: true }; case PostTypes.SHOW_POST_SUCCESS: tags = action.data.tags; if(tags) { tags = tags.map(x => x['id']); } else { tags = []; } action.data.tags = tags; action.data.image = ""; return { ...state, create_update_spinner: false, post: action.data }; case PostTypes.SHOW_POST_FAILURE: return { ...state, create_update_spinner: false, error_message: action.error.message }; case PostTypes.EDIT_POSTS: return { ...state, create_update_spinner: true }; case PostTypes.EDIT_POSTS_SUCCESS: tags = action.data.data.tags; if(tags) { tags = tags.map(x => x['id']); } else { tags = []; } action.data.data.tags = tags; return { ...state, post: action.data.data, create_update_spinner: false, success_message: action.data.message, error_message: "", validation_errors: {} }; case PostTypes.EDIT_POSTS_FAILURE: return { ...state, create_update_spinner: false, error_message: action.error.message, validation_errors: action.error.errors, success_message: "" }; case PostTypes.DELETE_POSTS: return { ...state, list_spinner: true }; case PostTypes.DELETE_POSTS_SUCCESS: let posts = state.posts; posts.data = state.posts.data.filter(item => item.id != action.id); return { ...state, list_spinner: false, posts: posts, success_message: action.message, error_message: '' }; case PostTypes.DELETE_POSTS_FAILURE: return { ...state, list_spinner: false, error_message: action.error.message, success_message: '' }; case PostTypes.SET_POST_DEFAULTS: return { ...state, success_message: "", error_message: "", validation_errors: {}, list_spinner: false, create_update_spinner: false }; case PostTypes.RESET_FIELDS: return { ...state, post: { id: "", title: "", slug: "", content: "", image: "", published: 1, category_id: "", tags: [] } }; default: return state; } }; function handleFieldChange(state, action) { if(action.field == 'title' || action.field == 'content' || action.field == 'category_id' || action.field == 'published' || action.field == 'image') { return { ...state, post: {...state.post, [action.field]: action.data} }; } else if(action.field == 'tag[]') { let selected_tags = state.post.tags; if(action.checked == true) { if(!selected_tags.includes(action.data)) { selected_tags.push(parseInt(action.data)); } } else if(action.checked == false) { if(selected_tags.includes(parseInt(action.data))) { selected_tags = selected_tags.filter(item => item != parseInt(action.data)) } } return { ...state, post: {...state.post, tags: selected_tags} }; } } export default postReducer;
As we did with any other reducer, the post reducer checks for the action type and then updating the related state data. In the initial state object we have posts which is an object to contain the posts in the listing page. Also the post object have attributes like id, title, slug etc. will store the post data when processing create and update.
The HANDLE_FIELD_CHANGE action check for the field then updating that field value accordingly, this is where the handleFieldChange() function do, for fields like title, category_id, content, image, and published we update the field normally as we done in categories, but for tags because this is an array of tag ids we check if the incoming value is already exist in the array or not then appending or removing it.
The final update to do is updating the root reducer.
Update resources/js/admin/store/reducers/RootReducer.js
import { combineReducers } from 'redux'; import categoryReducer from './CategoryReducer'; import tagReducer from './TagReducer'; import postReducer from './PostReducer'; const rootReducer = combineReducers({ category: categoryReducer, tag: tagReducer, post: postReducer }); export default rootReducer;
Here we add the postReducer with an alias of post so we can access post store in any component by using props like this ‘this.props.post‘. In the next section we will create the components for listing, create, update, and delete.
Posts Components
List All Posts
Open resources/js/admin/components/pages/posts/Index.js and update it like this:
import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; // actions import { listPosts, setPostDefaults } from '../../../store/actions/PostActions'; // partials import Breadcrumb from '../../partials/Breadcrumb'; import Spinner from '../../partials/Spinner'; import Pagination from '../../partials/Pagination'; import SuccessAlert from '../../partials/SuccessAlert'; import ErrorAlert from '../../partials/ErrorAlert'; import Row from './Row'; class Index extends React.Component { constructor(props) { super(props); } componentDidMount() { this.props.setPostDefaults(); this.props.listPosts(1); } render() { return ( <div className="content-wrapper"> <section className="content-header"> <h1> Posts </h1> <Breadcrumb /> </section> <section className="content"> <div className="row"> <div className="col-md-12"> <div className="box"> <div className="box-header"> <h3 className="box-title">All posts</h3> <Link to='/posts/add' className="btn btn-primary pull-right">Add <i className="fa fa-plus"></i></Link> </div> <div className="box-body"> <Spinner show={this.props.post.list_spinner}/> <SuccessAlert msg={this.props.post.success_message}/> <ErrorAlert msg={this.props.post.error_message}/> <table className="table"> <thead> <tr> <th>#</th> <th>Title</th> <th>Image</th> <th>Published</th> <th>Category</th> <th>User</th> <th width="15%">Actions</th> </tr> </thead> <tbody> { this.props.post.posts.data? ( this.props.post.posts.data.map(item => <Row key={item.id} post={item} />) ) : null } </tbody> </table> </div> <Pagination data={this.props.post.posts} onclick={this.props.listPosts.bind(this)} /> </div> </div> </div> </section> </div> ); } } const mapStateToProps = (state, ownProps) => { return { post: state.post }; }; const mapDispatchToProps = (dispatch) => { return { listPosts: (page) => dispatch(listPosts(page)), setPostDefaults: () => dispatch(setPostDefaults()) }; }; export default connect(mapStateToProps, mapDispatchToProps)(Index);
Create this file resources/js/admin/components/pages/posts/Row.js and update with this code:
import React from 'react'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { deletePost } from '../../../store/actions/PostActions'; class Row extends React.Component { constructor(props) { super(props); this.handleDelete = this.handleDelete.bind(this); } handleDelete(e) { e.preventDefault(); if(confirm("Are you sure?")) { this.props.deletePost(this.props.post.id); } } render() { return ( <tr> <td>{this.props.post.id}</td> <td>{this.props.post.title}</td> <td> <img src={this.props.post.image_url} width="50" height="40" /> </td> <td> {this.props.post.published == 1?(<span className="badge bg-green">published</span>):(<span className="badge bg-gray">draft</span>)} </td> <td>{this.props.post.category?this.props.post.category.title:""}</td> <td>{this.props.post.user?this.props.post.user.name:""}</td> <td> <Link to={'/posts/edit/' + this.props.post.id} className="btn btn-info btn-sm"><i className="fa fa-edit"></i></Link> <a href="#" className="btn btn-danger btn-sm" onClick={this.handleDelete}><i className="fa fa-remove"></i></a> </td> </tr> ) } }; const mapDispatchToProps = (dispatch) => { return { deletePost: (id) => dispatch(deletePost(id)) } }; export default connect(null, mapDispatchToProps)(Row);
Creating Posts
To create new posts we will use CKEditor rich text editor plugin, you can install the plugin using this command:
npm install --save @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
This command installs ckeditor for react and you can pick a build to install, in this case i chosen the classic build.
Next we need to create a special style to control the styling for the editor. Create new directory css/ inside of resources/js/admin/ and create this file resources/js/admin/css/editor.css then add this style:
.ck-editor__editable { min-height: 300px; }
Now let’s imagine the post form we need to implement, the screenshot shown below represent the form we will build next
The post form composed of two parts, the first contain the title and content. The other part contain category, tags, image and two buttons for publish or saving draft. Note here beside categories dropdown there is a button to create new category so it can show instantly in the dropdown, this is also the case for tags.
First let’s return back to categories and tags, we need to add a new component to show a modal when the user click on add new category and add new tag.
Create resources/js/admin/components/pages/categories/AddModal.js and update it like this:
import React from 'react'; import {connect} from 'react-redux'; import { addCategory, setCategoryDefaults, handleCategoryTitle, listAllCategories } from '../../../store/actions/CategoryActions'; import CategoryForm from './CategoryForm'; class AddModal extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } componentDidMount() { this.props.setCategoryDefaults(); } handleChange(e) { e.preventDefault(); this.props.handleTitleChange(e.target.value); } handleSubmit(e) { e.preventDefault(); let self = this; this.props.addCategory(this.props.categories.category.title, function () { // reset title self.props.handleTitleChange(''); setTimeout(() => { // close modal self.props.close_modal(); // reset defaults self.props.setCategoryDefaults(); // refetch categories self.props.listAllCategories(); }, 2000); }); } render() { return ( <div className={`modal fade` + (this.props.show_modal==true?' in':'')} style={{display: (this.props.show_modal==true?'block':'none')}} id="modal-default"> <div className="modal-dialog"> <form role="form" method="post" onSubmit={this.handleSubmit}> <div className="modal-content"> <div className="modal-header"> <button type="button" className="close" aria-label="Close" onClick={this.props.close_modal}> <span aria-hidden="true">×</span></button> <h4 className="modal-title">Add new category</h4> </div> <div className="modal-body"> <CategoryForm categories={this.props.categories} onchange={this.handleChange}/> </div> <div className="modal-footer"> <button type="button" className="btn btn-default pull-left" onClick={this.props.close_modal}>Close</button> <button type="submit" className="btn btn-primary">Save</button> </div> </div> </form> </div> </div> ) } } const mapStateToProps = (state, ownProps) => { return { categories: state.category }; }; const mapDispatchToProps = (dispatch) => { return { handleTitleChange: (title) => dispatch(handleCategoryTitle(title)), addCategory: (title, cb) => dispatch(addCategory(title, cb)), setCategoryDefaults: () => dispatch(setCategoryDefaults()), listAllCategories: () => dispatch(listAllCategories()), }; }; export default connect(mapStateToProps, mapDispatchToProps)(AddModal);
Also create resources/js/admin/components/pages/tags/AddModal.js with this code:
import React from 'react'; import {connect} from 'react-redux'; import { addTag, setTagDefaults, handleTagTitle, listAllTags } from '../../../store/actions/TagActions'; import TagForm from './TagForm'; class AddModal extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } componentDidMount() { this.props.setTagDefaults(); } handleChange(e) { e.preventDefault(); this.props.handleTitleChange(e.target.value); } handleSubmit(e) { e.preventDefault(); let self = this; this.props.addTag(this.props.tag.tag.title, function () { // reset title self.props.handleTitleChange(''); setTimeout(() => { // close modal self.props.close_modal(); // reset defaults self.props.setTagDefaults(); // refetch tags self.props.listAllTags(); }, 2000); }); } render() { return ( <div className={`modal fade` + (this.props.show_modal==true?' in':'')} style={{display: (this.props.show_modal==true?'block':'none')}} id="modal-default"> <div className="modal-dialog"> <form role="form" method="post" onSubmit={this.handleSubmit}> <div className="modal-content"> <div className="modal-header"> <button type="button" className="close" aria-label="Close" onClick={this.props.close_modal}> <span aria-hidden="true">×</span></button> <h4 className="modal-title">Add new tag</h4> </div> <div className="modal-body"> <TagForm tag={this.props.tag} onchange={this.handleChange}/> </div> <div className="modal-footer"> <button type="button" className="btn btn-default pull-left" onClick={this.props.close_modal}>Close</button> <button type="submit" className="btn btn-primary">Save</button> </div> </div> </form> </div> </div> ) } } const mapStateToProps = (state, ownProps) => { return { tag: state.tag }; }; const mapDispatchToProps = (dispatch) => { return { handleTitleChange: (title) => dispatch(handleTagTitle(title)), addTag: (title, cb) => dispatch(addTag(title, cb)), setTagDefaults: () => dispatch(setTagDefaults()), listAllTags: () => dispatch(listAllTags()), }; }; export default connect(mapStateToProps, mapDispatchToProps)(AddModal);
These two modals represent the same component of adding new categories and new tags so i used the same partial component for each modal which is the CategoryForm and TagForm.
Now after i added the modals for creating categories and tags, update resources/js/admin/components/pages/posts/Add.js
import React from 'react'; import { connect } from 'react-redux'; // style import '../../../css/editor.css'; // partials import Breadcrumb from '../../partials/Breadcrumb'; import AddCategoryModal from '../categories/AddModal'; import AddTagModal from '../tags/AddModal'; import PostForm from './PostForm'; // actions import { listAllCategories } from '../../../store/actions/CategoryActions'; import { listAllTags } from '../../../store/actions/TagActions'; import { handleFieldChange, addPost, setPostDefaults, resetFields } from '../../../store/actions/PostActions'; class Add extends React.Component { constructor(props) { super(props); this.state = { show_add_category_modal: false, show_add_tag_modal: false }; this.submitRef = React.createRef(); this.handleFieldChange = this.handleFieldChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleSave = this.handleSave.bind(this); } componentDidMount() { this.props.setPostDefaults(); this.props.resetFields(); this.props.listAllCategories(); this.props.listAllTags(); } openAddCategoryModal() { this.setState({ show_add_category_modal: true }); } closeAddCategoryModal() { this.setState({ show_add_category_modal: false }); } openAddTagModal() { this.setState({ show_add_tag_modal: true }); } closeAddTagModal() { this.setState({ show_add_tag_modal: false }); } handleFieldChange(e) { if(e.target.name == 'tag[]') { this.props.handleFieldChange(e.target.name, e.target.value, e.target.checked); } else if(e.target.name == 'image') { this.props.handleFieldChange(e.target.name, e.target.files[0]); } else { this.props.handleFieldChange(e.target.name, e.target.value); } } handleCkeditorChange(editor) { this.props.handleFieldChange("content", editor.getData()); } handleSubmit(e) { e.preventDefault(); let self = this; this.props.addPost(this.props.post.post, function () { // reset fields self.props.resetFields(); // redirect setTimeout(() => self.props.history.push('/posts'), 2000); }); } handleSave(e) { e.preventDefault(); this.props.handleFieldChange('published', e.target.name=='publish'?1:2); setTimeout(() => this.submitRef.current.click(), 200); } render() { return ( <div className="content-wrapper"> <section className="content-header"> <h1> Add Post </h1> <Breadcrumb /> </section> <section className="content"> <div className="row"> <form method="post" role="form" onSubmit={this.handleSubmit}> <PostForm post={this.props.post.post} create_update_spinner={this.props.post.create_update_spinner} success_message={this.props.post.success_message} error_message={this.props.post.error_message} handleFieldChange={this.handleFieldChange} handleCkeditorChange={(event, editor) => this.handleCkeditorChange(editor)} all_categories={this.props.all_categories} all_tags={this.props.all_tags} openAddCategoryModal={this.openAddCategoryModal.bind(this)} openAddTagModal={this.openAddTagModal.bind(this)} handleSave={this.handleSave} submitRef={this.submitRef} validation_errors={this.props.post.validation_errors} /> </form> </div> </section> <AddCategoryModal show_modal={this.state.show_add_category_modal} close_modal={this.closeAddCategoryModal.bind(this)} /> <AddTagModal show_modal={this.state.show_add_tag_modal} close_modal={this.closeAddTagModal.bind(this)} /> </div> ); } } const mapStateToProps = (state, ownProps) => { return { all_categories: state.category.all_categories, all_tags: state.tag.all_tags, post: state.post }; }; const mapDispatchToProps = (dispatch) => { return { addPost: (payload, cb) => dispatch(addPost(payload, cb)), listAllCategories: () => dispatch(listAllCategories()), listAllTags: () => dispatch(listAllTags()), handleFieldChange: (field, value, checked = null) => dispatch(handleFieldChange(field, value, checked)), setPostDefaults: () => dispatch(setPostDefaults()), resetFields: () => dispatch(resetFields()) } }; export default connect(mapStateToProps, mapDispatchToProps)(Add);
Create this file resources/js/admin/components/pages/posts/PostForm.js with the code shown below:
import React from 'react'; import CKEditor from '@ckeditor/ckeditor5-react'; import ClassicEditor from "@ckeditor/ckeditor5-build-classic/build/ckeditor"; import { Link } from 'react-router-dom'; import Spinner from '../../partials/Spinner'; import SuccessAlert from '../../partials/SuccessAlert'; import ErrorAlert from '../../partials/ErrorAlert'; class Form extends React.Component { constructor(props) { super(props); } render() { return ( <div> <Spinner show={this.props.create_update_spinner}/> <SuccessAlert msg={this.props.success_message}/> <ErrorAlert msg={this.props.error_message}/> <div className="col-md-8"> <div className="box box-warning"> <div className="box-header with-border"> <h3 className="box-title">{this.props.post.id!=""?'Edit post #'+this.props.post.id:'Add post'}</h3> <Link to='/posts' className="btn btn-warning btn-sm pull-right"><i className="fa fa-arrow-left"></i> Return back</Link> </div> <div className="box-body"> <div className={`form-group ${this.props.validation_errors.title?'has-error':''}`}> <label>Post title</label> <input type="text" className="form-control" placeholder="Post title" onChange={this.props.handleFieldChange} value={this.props.post.title?this.props.post.title:''} name="title" /> { this.props.validation_errors.title!=null?(<div className="help-block">{this.props.validation_errors.title[0]}</div>):null } </div> <div className={`form-group ${this.props.validation_errors.content?'has-error':''}`}> <label>Content</label> <CKEditor name="content" editor={ ClassicEditor } data={this.props.post.content?this.props.post.content:''} onInit={(editor) => { editor.setData(this.props.post.content?this.props.post.content:'') }} onChange={this.props.handleCkeditorChange} /> { this.props.validation_errors.content!=null?(<div className="help-block">{this.props.validation_errors.content[0]}</div>):null } </div> </div> </div> </div> <div className="col-md-4"> <div className="box box-success"> <div className="box-body"> <div className={`input-group input-group-sm ${this.props.validation_errors.category_id?'has-error':''}`}> <select name="category_id" id="category_id" className="form-control" onChange={this.props.handleFieldChange} value={this.props.post.category_id}> <option value="">select category</option> { this.props.all_categories.map(cat => { return ( <option key={cat.id} value={cat.id}>{cat.title}</option> ) }) } </select> <span className="input-group-btn"> <button type="button" className="btn btn-info btn-flat" onClick={this.props.openAddCategoryModal.bind(this)}><i className="fa fa-plus"></i> Add new category</button> </span> </div> { this.props.validation_errors.category_id!=null?(<div className="help-block">{this.props.validation_errors.category_id[0]}</div>):null } <br/> <div className="form-group"> <label>Tags</label> <div> { this.props.all_tags.map(tag => { return ( <div className="checkbox" key={tag.id}> <label> <input type="checkbox" name="tag[]" value={tag.id} onChange={this.props.handleFieldChange} checked={this.props.post.tags.includes(tag.id)} /> { tag.title } </label> </div> ) }) } </div> <button type="button" className="btn btn-info btn-flat" onClick={this.props.openAddTagModal.bind(this)}><i className="fa fa-plus"></i> Add new tag</button> </div> { this.props.post.image_url?( <img src={this.props.post.image_url} width="100" height="80" /> ): null } <div className={`form-group ${this.props.validation_errors.image?'has-error':''}`}> <label>Image</label> <input type="file" name="image" id="image" className="form-control" onChange={this.props.handleFieldChange} accept="image/*" /> { this.props.validation_errors.image!=null?(<div className="help-block">{this.props.validation_errors.image[0]}</div>):null } </div> <div className="row"> <div className="col-md-6"><input type="button" name="publish" value="Publish" onClick={this.props.handleSave} className="btn btn-success" /></div> <div className="col-md-6"><input type="button" name="savedraft" value="Save draft" onClick={this.props.handleSave} className="btn btn-default pull-right" /></div> <input type="submit" ref={this.props.submitRef} style={{display: 'none'}} /> </div> </div> </div> </div> </div> ) } } export default Form;
The post form is too big, in the constructor i added a state object which have two attributes which controls showing or hiding the add category and add tag modal.
In the componentDidMount() hook first is called setPostDefaults() and resetFields(), this is an essential operation to reset fields when i navigate each time to add new post, then i called listAllCategories() and listAllTags() to fetch categories to be displayed in the dropdown and tags to be displayed in checkbox groups.
I added multiple functions to control the modals visibility like openAddCategoryModal(), openAddTagModal(), closeAddCategoryModal(), closeAddTagModal(). Those functions triggered when user clicks on add new category or add new tag.
The handleFieldChange() checks for the field name, then calling handleFieldChange prop coming from the store. handleCkeditor() function the same as handleFieldChange() but used only in case of ckeditor. Finally the handleSubmit() triggered when the user submit the form and save the post.
handleSave() function used to set the published field to whether 1 or 2 depending on whether the user click published or save draft buttons, after that we trigger click on the button using this.submitRef.current.click().
In the render() function i extracted the form into a partial component <PostForm/> and passed to it a lot of props like shown above like the post object, spinners, categories, etc. Then i displayed the <AddCategoryModal /> and <AddTagModal /> components.
Updating Posts
Update resources/js/admin/components/pages/posts/Edit.js as follows:
import React from 'react'; import { connect } from 'react-redux'; // style import '../../../css/editor.css'; // partials import Breadcrumb from '../../partials/Breadcrumb'; import AddCategoryModal from '../categories/AddModal'; import AddTagModal from '../tags/AddModal'; import PostForm from './PostForm'; // actions import { listAllCategories } from '../../../store/actions/CategoryActions'; import { listAllTags } from '../../../store/actions/TagActions'; import { handleFieldChange, showPost, editPost, setPostDefaults, resetFields } from '../../../store/actions/PostActions'; class Edit extends React.Component { constructor(props) { super(props); this.state = { show_add_category_modal: false, show_add_tag_modal: false }; this.submitRef = React.createRef(); this.handleFieldChange = this.handleFieldChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleSave = this.handleSave.bind(this); } componentDidMount() { this.props.setPostDefaults(); this.props.listAllCategories(); this.props.listAllTags(); this.props.showPost(this.props.match.params.id); } openAddCategoryModal() { this.setState({ show_add_category_modal: true }); } closeAddCategoryModal() { this.setState({ show_add_category_modal: false }); } openAddTagModal() { this.setState({ show_add_tag_modal: true }); } closeAddTagModal() { this.setState({ show_add_tag_modal: false }); } handleFieldChange(e) { if(e.target.name == 'tag[]') { this.props.handleFieldChange(e.target.name, e.target.value, e.target.checked); } else if(e.target.name == 'image') { this.props.handleFieldChange(e.target.name, e.target.files[0]); } else { this.props.handleFieldChange(e.target.name, e.target.value); } } handleCkeditorChange(editor) { this.props.handleFieldChange("content", editor.getData()); } handleSubmit(e) { e.preventDefault(); let self = this; this.props.editPost(this.props.match.params.id, this.props.post.post, function () { // reset fields self.props.resetFields(); // redirect setTimeout(() => self.props.history.push('/posts'), 2000); }); } handleSave(e) { e.preventDefault(); this.props.handleFieldChange('published', e.target.name=='publish'?1:2); setTimeout(() => this.submitRef.current.click(), 200); } render() { return ( <div className="content-wrapper"> <section className="content-header"> <h1> Edit Post </h1> <Breadcrumb /> </section> <section className="content"> <div className="row"> <form method="post" role="form" onSubmit={this.handleSubmit}> <PostForm post={this.props.post.post} create_update_spinner={this.props.post.create_update_spinner} success_message={this.props.post.success_message} error_message={this.props.post.error_message} handleFieldChange={this.handleFieldChange} handleCkeditorChange={(event, editor) => this.handleCkeditorChange(editor)} all_categories={this.props.all_categories} all_tags={this.props.all_tags} openAddCategoryModal={this.openAddCategoryModal.bind(this)} openAddTagModal={this.openAddTagModal.bind(this)} handleSave={this.handleSave} submitRef={this.submitRef} validation_errors={this.props.post.validation_errors} /> </form> </div> </section> <AddCategoryModal show_modal={this.state.show_add_category_modal} close_modal={this.closeAddCategoryModal.bind(this)} /> <AddTagModal show_modal={this.state.show_add_tag_modal} close_modal={this.closeAddTagModal.bind(this)} /> </div> ); } } const mapStateToProps = (state, ownProps) => { return { all_categories: state.category.all_categories, all_tags: state.tag.all_tags, post: state.post }; }; const mapDispatchToProps = (dispatch) => { return { showPost: (id) => dispatch(showPost(id)), editPost: (id, payload, cb) => dispatch(editPost(id, payload, cb)), listAllCategories: () => dispatch(listAllCategories()), listAllTags: () => dispatch(listAllTags()), handleFieldChange: (field, value, checked = null) => dispatch(handleFieldChange(field, value, checked)), setPostDefaults: () => dispatch(setPostDefaults()), resetFields: () => dispatch(resetFields()) } }; export default connect(mapStateToProps, mapDispatchToProps)(Edit);
The Edit component is the same as the Add component except that we fetch the current using the id in the componentDidMount().
Great now let’s try this open terminal and run
npm run dev or npm run watch
Continue to part8: Admin Comments
In PostForm.js, when I use CKeditor these errors are thrown. But without CKeditor it runs. Can you help me?
Seems like you missing something
sorry, i am facing error when i tried to hit button add post, can you please help me to find whats the problem
From the error seems that you missed something. read the article carefully