Building a Blog With Reactjs And Laravel Part5 Redux

Building a Blog With Reactjs And Laravel Part5: React Redux

In this article of this series i will prepare the react Redux store for use with reactjs and setup the different actions and reducers for the store.

 

 

 

In the previous article we prepared the react components and made the routes required to navigate to each component, also we implemented the login component to prevent unauthorized access to protected resources and admin panel. We will continue in this article by implementing an important aspect for any React app which is the react redux store. You should have knowledge of redux and react-redux in order to get the idea from this article.

Let’s install redux dependencies:

npm install redux react-redux redux-thunk

The redux and react-redux are required to work with redux to setup the store, the redux-thunk is required if you intend to work with async operations such as ajax requests.

Next create a new directory in the admin/ directory called store/ which represent our redux store. The store/ directory contains three directories:

  • actions
  • actionTypes
  • reducers

You should be familiar with the redux concepts, the actions represent functions that dispatch actions which in turn sent to reducers, finally the reducer update the state based on the action type. Each action must have a unique type, which will be used in the reducer function to check for this type thereby updating the related state. Here i added actions, actionTypes and reducers in separate directories.

 

In the actionTypes/ directory create these files:

  • CategoryTypes.js
  • CommentTypes.js
  • PostTypes.js
  • TagTypes.js
  • UserTypes.js

As you see for each module i created a separate file that represent the action types for this module, this is a good practice in large projects.

Also In the actions/ directory create these files:

  • CategoryActions.js
  • CommentActions.js
  • PostActions.js
  • TagActions.js
  • UserActions.js

Similarly In the reducers/ directory create these files:

  • CategoryReducer.js
  • CommentReducer.js
  • PostReducer.js
  • TagReducer.js
  • UserReducer.js
  • RootReducer.js

Let’s start with categories as the first module in our app, but first we need to create the apis needed to manipulate categories from the server (CRUD) such as create, update, delete and list.

Create resources/js/admin/apis/Category.js with the below code:

import axios from "axios";

const Category = {
    list: (page = 1) => {
        return axios.get('/categories?page=' + page);
    },
    add: (title) => {
        return axios.post('/categories', {title}, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token")}});
    },
    showOne: (id) => {
        return axios.get('/categories/' + id);
    },
    edit: (title, id) => {
        return axios.put('/categories/' + id, {title}, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token")}});
    },
    remove: (id) => {
        return axios.delete('/categories/' + id, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token")}});
    },
    listAll: () => {          // used to populate dropdowns
        return axios.get('/categories?all=1');
    }
};

export default Category;

This is the operations we will use for category CRUD, as you see i added all the operations in a single object Category so that we can call each method using Category.<operation> such as Category.add().

The next thing is to specify the redux action types for the category module

So open resources/js/admin/store/actionTypes/CategoryTypes.js

export const LIST_CATEGORIES = 'LIST_CATEGORIES';
export const LIST_CATEGORIES_SUCCESS = 'LIST_CATEGORIES_SUCCESS';
export const LIST_CATEGORIES_FAILURE = 'LIST_CATEGORIES_FAILURE';
export const CREATE_CATEGORIES = 'CREATE_CATEGORIES';
export const CREATE_CATEGORIES_SUCCESS = 'CREATE_CATEGORIES_SUCCESS';
export const CREATE_CATEGORIES_FAILURE = 'CREATE_CATEGORIES_FAILURE';
export const EDIT_CATEGORIES = 'EDIT_CATEGORIES';
export const EDIT_CATEGORIES_SUCCESS = 'EDIT_CATEGORIES_SUCCESS';
export const EDIT_CATEGORIES_FAILURE = 'EDIT_CATEGORIES_FAILURE';
export const DELETE_CATEGORIES = 'DELETE_CATEGORIES';
export const DELETE_CATEGORIES_SUCCESS = 'DELETE_CATEGORIES_SUCCESS';
export const DELETE_CATEGORIES_FAILURE = 'DELETE_CATEGORIES_FAILURE';
export const SHOW_CATEGORY = 'SHOW_CATEGORY';
export const SHOW_CATEGORY_SUCCESS = 'SHOW_CATEGORY_SUCCESS';
export const SHOW_CATEGORY_FAILURE = 'SHOW_CATEGORY_FAILURE';
export const HANDLE_CATEGORY_TITLE = 'HANDLE_CATEGORY_TITLE';
export const SET_CATEGORY_DEFAULTS = 'SET_CATEGORY_DEFAULTS';
export const LIST_ALL_CATEGORIES = 'LIST_ALL_CATEGORIES';    // this is different from LIST_CATEGORIES

As a rule of thumb always specify the actions as constants in separate files and export them as show above, this makes the code more cleaner. As you see i specify different actions so i can update the state with different data depending on the action type.

For example if we take ‘create category’  as an example as shown above i specified three types which are (CREATE_CATEGORIES, CREATE_CATEGORIES_SUCCESS, CREATE_CATEGORIES_FAILURE), why i created three types? You might think when you create category the first thing is to display a spinner to the user which represent action type(CREATE_CATEGORIES), the next thing you have two cases whether the operation succeeded of failed, this is where the action type (CREATE_CATEGORIES_SUCCESS) used with success, or (CREATE_CATEGORIES_FAILURE) used with failure, the other action types used similarly.

There is a special action which is LIST_ALL_CATEGORIES. We will use this action in cases when we need to fetch all categories for example to populate select dropdown.

Displaying all Categories

I will begin with displaying all categories so we will create the actions for this.

Open resources/js/admin/store/actions/CategoryActions.js and add the below code:

import { LIST_CATEGORIES,
    LIST_CATEGORIES_SUCCESS,
    LIST_CATEGORIES_FAILURE,
    SET_CATEGORY_DEFAULTS
} from '../actionTypes/CategoryTypes';

import Category from '../../apis/Category';

/**
 * list Categories action
 */
function listCategories(page = 1) {
    
    return function (dispatch, getState) {

        // start sending request (first dispatch)
        dispatch({
            type: LIST_CATEGORIES
        });


        // async call must dispatch action whether on success or failure
        Category.list(page).then(response => {
            dispatch({
                type: LIST_CATEGORIES_SUCCESS,
                data: response.data.data
            });
        }).catch(error => {
            dispatch({
                type: LIST_CATEGORIES_FAILURE,
                error: error.response.data
            });
        });
    }
}

function setCategoryDefaults() {

    return function (dispatch, getState) {

        dispatch({
            type: SET_CATEGORY_DEFAULTS
        });
    }
}

export {
    listCategories,
    setCategoryDefaults
};

As you see above CategoryActions.js will contain all the actions for category, for now i added only two actions ‘listCategories‘ and ‘setCategoryDefaults‘. The action is a function that dispatch action type in other means it sends the action to the reducer.

In redux context the action function must return another function as in listCategories action. The returned function takes two arguments which are the (dispatch, getState). The dispatch parameter is a callback that enable us to dispatch action type, and the getState parameter enable us to access the state.The dispatch callback takes an object which has a type and whatever other data you want to send like this:

dispatch({
            type: LIST_CATEGORIES
});

dispatch({
                type: LIST_CATEGORIES_SUCCESS,
                data: response.data.data
            });

 

In listCategories() action first i dispatched action type ‘LIST_CATEGORIES‘ which we discussed above in the action types. This action type we will use to display a spinner, next we called async process which is the Api to get the categories. This api return a javascript promise which we also dispatch another action types ‘LIST_CATEGORIES_SUCCESS’ or ‘LIST_CATEGORIES_FAILURE’ depending on the success or failure of the operation.

The setCategoryDefaults() action will be used to reset the state, for example when you visit the add category page you have to hide the spinner and any other messages, so we will trigger that action here.

 

Open resources/js/admin/store/reducers/CategoryReducer.js

import { LIST_CATEGORIES,
    LIST_CATEGORIES_SUCCESS,
    LIST_CATEGORIES_FAILURE,
    SET_CATEGORY_DEFAULTS
} from '../actionTypes/CategoryTypes';

const initialState = {
    categories: {},
    category: {
        id: "",
        title: "",
        slug: ""
    },
    success_message: "",
    error_message: "",
    validation_errors: null,
    list_spinner: false,
    create_update_spinner: false
};

const categoryReducer = function (state = initialState, action) {
     switch (action.type) {
           case LIST_CATEGORIES:
            return {
                ...state,
                list_spinner: true
            };
        case LIST_CATEGORIES_SUCCESS:
            return {
                ...state,
                categories: action.data,
                list_spinner: false
            };
        case LIST_CATEGORIES_FAILURE:
            return {
                ...state,
                error_message: action.error,
                list_spinner: false
            };
        case SET_CATEGORY_DEFAULTS:
            return {
                ...state,
                category: {...state.category},
                success_message: "",
                error_message: "",
                validation_errors: null,
                list_spinner: false,
                create_update_spinner: false
            };
        default:
            return state;
     }
};

export default categoryReducer;

In the reducer first we declared the state for the module we are dealing with in this case categories. The state is a simple object which contain the state data. In this case our state object contains this:

const initialState = {
    categories: {},
    category: {
        id: "",
        title: "",
        slug: ""
    },
    success_message: "",
    error_message: "",
    validation_errors: null,
    list_spinner: false,
    create_update_spinner: false
};

The ‘categories‘ key store the categories that will be displayed in the listing page. The ‘category‘ store specific category which will be stored when create new category or modify existing category. The other data like ‘success_message‘, ‘error_message‘ and spinners used to show errors or status messages and spinners.

Next the reducer is a function which takes (state, action) and must update the state depending on the action.type, The state argument which be default is initialState we declared above and the action argument enable us to check for the action type and thereby updating the state accordingly.

Every reducer functionmust have a switch statement or if else to check for action types, in our case we use switch statement in categoryReducer.

So in LIST_CATEGORIES case i return this:

return {
     ...state,
     list_spinner: true
};

I use object destruction expression here …state. Then i changed list_spinner to be true, we will use this in the component to display spinner.

Next in LIST_CATEGORIES_SUCCESS :

return {
                ...state,
                categories: action.data,
                list_spinner: false
            };

I returned the same state and in addition i updated categories with action.data, this data coming from the action as you saw above. Also i updated list_spinner to be false so that we can hide the spinner in the component.

In LIST_CATEGORIES_FAILURE:

return {
                ...state,
                error_message: action.error,
                list_spinner: false
            };

Here i updated the error_message to show the error and also hide the spinner.

The SET_CATEGORY_DEFAULTS type we reset the state to the default state which for example setting errors and success messages to null and spinners to be false.

Note that you must not update the state directly always return a new state, but never mutate the state directly like this:

case LIST_CATEGORIES:
            state.list_spinner = true;    // wrong

But this is correct:

case LIST_CATEGORIES:
            return {
                ...state,
                list_spinner: true
            };

Also the switch statement must have a default case which return the default state as you see above.

 

Now we need to update the rootReducer, the root reducer combine all reducers into one object. Open resources/js/admin/store/reducers/RootReducer.js and add this code:

import { combineReducers } from 'redux';

import categoryReducer  from './CategoryReducer';

const rootReducer = combineReducers({
   category: categoryReducer
});

export default rootReducer;

The combineReducer utility i imported from redux. It takes an object of other reducers and combine them into one object aliased with an alias, in this case i added alias ‘category‘ to refer to categoryReducer. Now we can access the state in any component with state.category.<key>

 

Update resources/js/admin/App.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import {
    HashRouter as Router,
    Link,
    Route,
    Switch
} from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk'
import rootReducer from './store/reducers/RootReducer';
import Header from './components/partials/Header';
import Sidebar from './components/partials/Sidebar';
import Footer from './components/partials/Footer';
import Routes from './Routes';

const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));


class App extends Component {
    render() {
        return (
            <Provider store={store}>
                <Router>
                        <div className="wrapper">
                            <Header/>
                            <Sidebar/>

                            <Routes/>

                            <Footer/>
                        </div>
                </Router>
            </Provider>
        );
    }
}

export default App;

if (document.getElementById('app')) {
    ReactDOM.render(<App />, document.getElementById('app'));
}

In the App component i imported <Provider/> component from react-redux, this component must enclose our App component and takes a prop called store which is store created with createStore() function. The createStore() takes the rootReducer and optional parameters. in this case the optional parameter is thunk middleware, this middleware used to dispatch actions in async operations.

The final thing is to update the component to show all categories. Open resources/js/admin/components/pages/categories/Index.js

import React from 'react';
import { connect } from 'react-redux';
import Breadcrumb from '../../partials/Breadcrumb';
import { listCategories, setCategoryDefaults } from '../../../store/actions/CategoryActions';
import Spinner from '../../partials/Spinner';
import Row from './Row';
import { Link } from 'react-router-dom';
import Pagination from '../../partials/Pagination';
import SuccessAlert from '../../partials/SuccessAlert';
import ErrorAlert from '../../partials/ErrorAlert';

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

    componentDidMount() {
        this.props.setCategoryDefaults();

        this.props.listCategories(1);
    }

    render()
    {
        return (
            <div className="content-wrapper">
                <section className="content-header">
                    <h1>
                        Categories
                    </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 categories</h3>

                                    <Link to='/categories/add' className="btn btn-primary pull-right">Add <i className="fa fa-plus"></i></Link>
                                </div>

                                <div className="box-body">
                                    <Spinner show={this.props.categories.list_spinner}/>

                                    <SuccessAlert msg={this.props.categories.success_message}/>
                                    <ErrorAlert msg={this.props.categories.error_message}/>

                                    <table className="table">
                                        <thead>
                                            <tr>
                                                <th>#</th>
                                                <th>Title</th>
                                                <th>Slug</th>
                                                <th width="15%">Actions</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                        {
                                            this.props.categories.categories.data?(
                                                this.props.categories.categories.data.map(item => <Row key={item.id} category={item} />)
                                            ):null
                                        }
                                        </tbody>
                                    </table>
                                </div>

                                <Pagination data={this.props.categories.categories} onclick={this.props.listCategories.bind(this)} />

                            </div>
                        </div>
                    </div>
                </section>
            </div>
        );
    }
}

const mapStateToProps = (state, ownProps) => {

    return {
        categories: state.category
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        listCategories: (page) => dispatch(listCategories(page)),
        setCategoryDefaults: () => dispatch(setCategoryDefaults())
    }
};

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

There are a lot of stuff in the above code but don’t be shocked once just keep focus and you will understand the idea. As you might think we want to access the redux state in this component. Redux provides us utility called ‘connect‘.

The connect utility is a higher order components, higher order components in React something like the decorator pattern which decorate the target components with extra props. In our case the connect transform the state to props and also transform the dispatch functions to props so that web can access anything inside components with props.

For this to work we created two functions mapStateToProps(state, ownProps), and mapDispatchToProps(dispatch). The mapStateToProps() return an object of the state we want to access in our component, in this case we want to access categories, so we returned categories which refers to state.category, remember that this is the alias we assign to category in rootReducer above.

Likewise the mapDispatchToProps() also return an object but this time object contains functions instead of state so i can call these functions for example i added listCategories(page) which when we call it will dispatch listCategories() action.

Then we pass mapStateToProps and mapDispatchToProps to the connect when exporting component like this:

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

With this we can now access the state and functions using props as you see above in componentDidMount():

componentDidMount() {
        this.props.setCategoryDefaults();

        this.props.listCategories(1);
}

<Spinner show={this.props.categories.list_spinner}/>

Create the below helper components:

Create resources/js/admin/components/partials/Spinner.js:

import React from 'react';

const Spinner = (props) => {
  return props.show?(
      <div className="overlay">
          <i className="fa fa-refresh fa-spin"></i>
      </div>
  ):null;
};

export default Spinner;

Create resources/js/admin/components/partials/SuccessAlert.js:

import React from 'react';

const SuccessAlert = (props) => {

    return props.msg!==""? (
        <div className="alert alert-success">
            {props.msg}
        </div>
    ) : null
};

export default SuccessAlert;

Create resources/js/admin/components/partials/ErrorAlert.js:

import React from 'react';

const ErrorAlert = (props) => {

    return props.msg!==''? (
        <div className="alert alert-danger">
            {props.msg}
        </div>
    ) : null
};

export default ErrorAlert;

Create resources/js/admin/components/partials/Pagination.js:

import React from 'react';
import PaginationItem from './PaginationItem';

const Pagination = (props) => {

    const num_pages = props.data.last_page;

    let pages = [];

    for (let page = 1; page <= num_pages; page++) {
        pages.push(<PaginationItem key={page} active={props.data.current_page==page} page={page} title={page} show={true} onclick={props.onclick} />);
    }

    return props.data && props.data.total > props.data.per_page?(
        <div className="box-footer clearfix">
            <ul className="pagination pagination-sm no-margin pull-right">
                <PaginationItem active={props.data.current_page==1} page="1" title="First" show={props.data.current_page > 1} onclick={props.onclick} />

                <PaginationItem active={false} title="«" page={props.data.current_page-1} show={props.data.current_page > 1} onclick={props.onclick} />

                {pages}

                <PaginationItem active={false} title="»" page={props.data.current_page+1} show={props.data.current_page < props.data.last_page} onclick={props.onclick} />

                <PaginationItem active={props.data.current_page==props.data.last_page} page={props.data.last_page} title="Last" show={props.data.current_page < props.data.last_page} onclick={props.onclick} />
            </ul>
        </div>
    ):null
};

export default Pagination;

Create resources/js/admin/components/partials/PaginationItem.js:

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

class PaginationItem extends React.Component {

    constructor(props)
    {
        super(props);
    }

    paginate(e) {
        e.preventDefault();

        this.props.onclick(this.props.page);
    }

    render() {
        return this.props.show ? (
            <li className={this.props.active ? 'active' : ''}>
                <a href="#" onClick={this.paginate.bind(this) }>{this.props.title}</a>
            </li>
        ) : null;
    }
}

export default connect(null, mapDispatchToProps)(PaginationItem);

Create resources/js/admin/components/pages/categories/Row.js:

import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';

class Row extends React.Component {

    constructor(props)
    {
        super(props);
    }

    render()
    {
        return (
            <tr>
                <td>{this.props.category.id}</td>
                <td>{this.props.category.title}</td>
                <td>
                    {this.props.category.slug}
                </td>
                <td>
                    <Link to={'/categories/edit/' + this.props.category.id} className="btn btn-info btn-sm"><i
                        className="fa fa-edit"></i></Link>
                    <a href="#" className="btn btn-danger btn-sm"><i
                        className="fa fa-remove"></i></a>
                </td>
            </tr>
        )
    }
};

export default Row;

Categories Complete

I think now you understand the idea behind redux let’s complete the categories CRUD.

Update resources/js/admin/store/actions/CategoryActions.js

import { LIST_CATEGORIES,
    LIST_CATEGORIES_SUCCESS,
    LIST_CATEGORIES_FAILURE,
    CREATE_CATEGORIES,
    CREATE_CATEGORIES_SUCCESS,
    CREATE_CATEGORIES_FAILURE,
    EDIT_CATEGORIES,
    EDIT_CATEGORIES_SUCCESS,
    EDIT_CATEGORIES_FAILURE,
    SHOW_CATEGORY,
    SHOW_CATEGORY_SUCCESS,
    SHOW_CATEGORY_FAILURE,
    DELETE_CATEGORIES,
    DELETE_CATEGORIES_SUCCESS,
    DELETE_CATEGORIES_FAILURE,
    SET_CATEGORY_DEFAULTS,
    HANDLE_CATEGORY_TITLE,
    LIST_ALL_CATEGORIES
} from '../actionTypes/CategoryTypes';

import Category from '../../apis/Category';

function handleCategoryTitle(title)
{
    return function (dispatch, getState) {

        dispatch({
            type: HANDLE_CATEGORY_TITLE,
            data: title
        })
    }
}

function setCategoryDefaults() {

    return function (dispatch, getState) {

        dispatch({
            type: SET_CATEGORY_DEFAULTS
        });
    }
}

/**
 * list Categories action
 */
function listCategories(page = 1) {
    
    return function (dispatch, getState) {

        // start sending request (first dispatch)
        dispatch({
            type: LIST_CATEGORIES
        });


        // async call must dispatch action whether on success or failure
        Category.list(page).then(response => {
            dispatch({
                type: LIST_CATEGORIES_SUCCESS,
                data: response.data.data
            });
        }).catch(error => {
            dispatch({
                type: LIST_CATEGORIES_FAILURE,
                error: error.response.data
            });
        });
    }
}

/**
 * add category action
 */
function addCategory (title, cb) {

    return function(dispatch, getState) {

        // start creation show spinner
        dispatch({
            type: CREATE_CATEGORIES
        });

        // async call must dispatch action whether on success or failure
        Category.add(title).then(response => {
            dispatch({
                type: CREATE_CATEGORIES_SUCCESS,
                data: response.data
            });

            cb();
        }).catch(error => {
            dispatch({
                type: CREATE_CATEGORIES_FAILURE,
                error: error.response.data
            })
        });
    }
}

/**
 * show category action
 */
function showCategory(id)
{
    return function (dispatch, getState) {
        // start creation show spinner
        dispatch({
            type: SHOW_CATEGORY
        });


        // async call must dispatch action whether on success or failure
        Category.showOne(id).then(response => {
            dispatch({
                type: SHOW_CATEGORY_SUCCESS,
                data: response.data
            });

        }).catch(error => {
            dispatch({
                type: SHOW_CATEGORY_FAILURE,
                error: error.response.data
            });
        });
    }
}

/**
 * edit category action
 */
function editCategory(title, id, cb)
{
    return function (dispatch, getState) {
        // start creation show spinner
        dispatch({
            type: EDIT_CATEGORIES
        });


        // async call must dispatch action whether on success or failure
        Category.edit(title, id).then(response => {
            dispatch({
                type: EDIT_CATEGORIES_SUCCESS,
                data: response.data
            });

            cb();
        }).catch(error => {
            dispatch({
                type: EDIT_CATEGORIES_FAILURE,
                error: error.response.data
            })
        });
    }
}

/**
 * delete category action
 */
function deleteCategory(id)
{
    return function (dispatch, getState) {

        // start creation show spinner
        dispatch({
            type: DELETE_CATEGORIES
        });


        // async call must dispatch action whether on success or failure
        Category.remove(id).then(response => {
            dispatch({
                type: DELETE_CATEGORIES_SUCCESS,
                message: response.data.message,
                id: id
            });
        }).catch(error => {
            dispatch({
                type: DELETE_CATEGORIES_FAILURE,
                error: error.response.data
            })
        });
    }
}

/**
 * list all categories action
 * this function used as a helper action for example to populate dropdowns
 * in other forms
 */
function listAllCategories() {

    return function (dispatch, getState) {

        // async call
        Category.listAll().then(response => {
            dispatch({
                type: LIST_ALL_CATEGORIES,
                data: response.data.data
            });
        });
    }
}

export {
    listCategories,
    handleCategoryTitle,
    addCategory,
    showCategory,
    editCategory,
    deleteCategory,
    setCategoryDefaults,
    listAllCategories
};

As you see above i added all the actions required to manipulate CRUD, let’s update the reducer.

Open resources/js/admin/store/reducers/CategoryReducer.js and update as shown:

import { LIST_CATEGORIES,
    LIST_CATEGORIES_SUCCESS,
    LIST_CATEGORIES_FAILURE,
    CREATE_CATEGORIES,
    CREATE_CATEGORIES_SUCCESS,
    CREATE_CATEGORIES_FAILURE,
    SHOW_CATEGORY,
    SHOW_CATEGORY_SUCCESS,
    SHOW_CATEGORY_FAILURE,
    EDIT_CATEGORIES,
    EDIT_CATEGORIES_SUCCESS,
    EDIT_CATEGORIES_FAILURE,
    DELETE_CATEGORIES,
    DELETE_CATEGORIES_SUCCESS,
    DELETE_CATEGORIES_FAILURE,
    SET_CATEGORY_DEFAULTS,
    HANDLE_CATEGORY_TITLE,
    LIST_ALL_CATEGORIES
} from '../actionTypes/CategoryTypes';

const initialState = {
    categories: {},        // used in listing page
    all_categories: [],    // used to fill dropdowns
    category: {
        id: "",
        title: "",
        slug: ""
    },
    success_message: "",
    error_message: "",
    validation_errors: null,
    list_spinner: false,
    create_update_spinner: false
};

const categoryReducer = function (state = initialState, action) {
    switch (action.type) {
        case SET_CATEGORY_DEFAULTS:
            return {
                ...state,
                category: {...state.category},
                success_message: "",
                error_message: "",
                validation_errors: null,
                list_spinner: false,
                create_update_spinner: false
            };
        case HANDLE_CATEGORY_TITLE:
            return {
                ...state,
                category: {...state.category, title: action.data}
            };
        case LIST_CATEGORIES:
            return {
                ...state,
                list_spinner: true
            };
        case LIST_CATEGORIES_SUCCESS:
            return {
                ...state,
                categories: action.data,
                list_spinner: false
            };
        case LIST_CATEGORIES_FAILURE:
            return {
                ...state,
                error_message: action.error,
                list_spinner: false
            };
        case LIST_ALL_CATEGORIES:
            return {
                ...state,
                all_categories: action.data
            };
        case CREATE_CATEGORIES:
            return {
                ...state,
                create_update_spinner: true
            };
        case CREATE_CATEGORIES_SUCCESS:
            return {
                ...state,
                create_update_spinner: false,
                category: action.data.data,
                success_message: action.data.message,
                error_message: "",
                validation_errors: null
            };
        case CREATE_CATEGORIES_FAILURE:
            return {
                ...state,
                create_update_spinner: false,
                error_message: action.error.message,
                validation_errors: action.error.errors,
                success_message: ""
            };
        case SHOW_CATEGORY:
            return {
                ...state,
                create_update_spinner: true
            };
        case SHOW_CATEGORY_SUCCESS:
            return {
                ...state,
                create_update_spinner: false,
                category: action.data.data
            };
        case SHOW_CATEGORY_FAILURE:
            return {
                ...state,
                create_update_spinner: false,
                error_message: action.error.message
            };
        case EDIT_CATEGORIES:
            return {
                ...state,
                create_update_spinner: true
            };
        case EDIT_CATEGORIES_SUCCESS:
            return {
                ...state,
                create_update_spinner: false,
                category: action.data.data,
                success_message: action.data.message,
                error_message: "",
                validation_errors: null
            };
        case EDIT_CATEGORIES_FAILURE:
            return {
                ...state,
                create_update_spinner: false,
                error_message: action.error.message,
                validation_errors: action.error.errors,
                success_message: ""
            };
        case DELETE_CATEGORIES:
            return {
                ...state,
                list_spinner: true
            };
        case DELETE_CATEGORIES_SUCCESS:
            let cats = state.categories;
            cats.data = state.categories.data.filter(item => item.id != action.id);

            return {
                ...state,
                list_spinner: false,
                categories: cats,
                success_message: action.message,
                error_message: ''
            };
        case DELETE_CATEGORIES_FAILURE:
            return {
                ...state,
                list_spinner: false,
                error_message: action.error.message,
                success_message: ''
            };
        default:
            return state;
    }
};

export default categoryReducer;

 

Creating Categories

Modify resources/js/admin/components/pages/categories/Add.js

import React from 'react';
import {connect} from 'react-redux';
import { Link } from 'react-router-dom';

// partials
import Breadcrumb from '../../partials/Breadcrumb';
import CategoryForm from './CategoryForm';

// actions
import { addCategory, setCategoryDefaults, handleCategoryTitle } from '../../../store/actions/CategoryActions';


class Add 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('');

            // redirect
            setTimeout(() => self.props.history.push('/categories'), 2000);
        });
    }

    render()
    {
        return (
            <div className="content-wrapper">
                <section className="content-header">
                    <h1>
                        Add category
                    </h1>

                    <Breadcrumb />

                </section>

                <section className="content">
                    <div className="row">
                        <div className="col-md-12">
                            <div className="box box-warning">
                                <div className="box-header with-border">
                                    <h3 className="box-title">Add category</h3>

                                    <Link to='/categories' className="btn btn-warning btn-sm"><i className="fa fa-arrow-left"></i> Return back</Link>
                                </div>
                                <form role="form" method="post" onSubmit={this.handleSubmit}>

                                    <div className="box-body">
                                         <CategoryForm categories={this.props.categories} onchange={this.handleChange}/>
                                    </div> 
                                    <div className="box-footer">
                                        <button type="submit" className="btn btn-success">Submit</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </section>
            </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())
    };
};

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

resources/js/admin/components/pages/categories/CategoryForm.js

import React from 'react';
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.categories.create_update_spinner}/>
                <SuccessAlert msg={this.props.categories.success_message}/>
                <ErrorAlert msg={this.props.categories.error_message}/>

                <div>
                    <div className={`form-group ${this.props.categories.validation_errors!=null?'has-error':''}`}>
                        <label>Category title</label>
                        <input type="text" className="form-control" placeholder="Category title" onChange={this.props.onchange} value={this.props.categories.category.title?this.props.categories.category.title:''} name="title" />
                        {
                            this.props.categories.validation_errors!=null?(<div className="help-block">{this.props.categories.validation_errors.title[0]}</div>):null
                        }
                    </div>
                </div>

            </div>
        )
    }
}

export default Form;

 

Updating Categories

Modify resources/js/admin/components/pages/categories/Edit.js

import React from 'react';
import {connect} from 'react-redux';
import { Link } from 'react-router-dom';

// partials
import Breadcrumb from '../../partials/Breadcrumb';
import CategoryForm from './CategoryForm';

// actions
import { showCategory, editCategory,
    setCategoryDefaults, handleCategoryTitle } from '../../../store/actions/CategoryActions';

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

        this.handleChange = this.handleChange.bind(this);

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

    componentDidMount()
    {
        this.props.setCategoryDefaults();

        this.props.showCategory(this.props.match.params.id);
    }

    handleChange(e) {
        e.preventDefault();

        this.props.handleTitleChange(e.target.value);
    }

    handleSubmit(e) {
        e.preventDefault();
        let self = this;

        this.props.editCategory(this.props.categories.category.title,
            this.props.match.params.id, function () {

                // reset title
                self.props.handleTitleChange('');

                // redirect
                setTimeout(() => self.props.history.push('/categories'), 2000);
            });
    }

    render()
    {
        return (
            <div className="content-wrapper">
                <section className="content-header">
                    <h1>
                        Edit category
                    </h1>

                    <Breadcrumb />

                </section>

                <section className="content">
                    <div className="row">
                        <div className="col-md-12">
                            <div className="box box-warning">
                                <div className="box-header with-border">
                                    <h3 className="box-title">Edit category #{ this.props.match.params.id }</h3>

                                    <Link to='/categories' className="btn btn-warning btn-sm"><i className="fa fa-arrow-left"></i> Return back</Link>
                                </div>
                                <form role="form" method="post" onSubmit={this.handleSubmit}>

                                    <div className="box-body">
                                        <CategoryForm categories={this.props.categories} onchange={this.handleChange}/>
                                    </div>
                                    <div className="box-footer">
                                        <button type="submit" className="btn btn-success">Update</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </section>
            </div>
        );
    }
}

const mapStateToProps = (state, ownProps) => {
    return {
        categories: state.category
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        showCategory: (id) => dispatch(showCategory(id)),
        handleTitleChange: (title) => dispatch(handleCategoryTitle(title)),
        editCategory: (title, id, cb) => dispatch(editCategory(title, id, cb)),
        setCategoryDefaults: () => dispatch(setCategoryDefaults())
    };
};

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

As you see above i extracted the form into a separate component <CategoryForm /> and reused it in Add and Edit components. This gives us the ability to reuse this form in other locations like to use it in a modal as we will see in future lessons when we make posts.

 

Deleting Categories

Modify resources/js/admin/components/pages/categories/Row.js

import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { deleteCategory } from '../../../store/actions/CategoryActions';

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.deleteCategory(this.props.category.id);
        }
    }

    render()
    {
        return (
            <tr>
                <td>{this.props.category.id}</td>
                <td>{this.props.category.title}</td>
                <td>
                    {this.props.category.slug}
                </td>
                <td>
                    <Link to={'/categories/edit/' + this.props.category.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 {
        deleteCategory: (id) => dispatch(deleteCategory(id))
    }
};

export default connect(null, mapDispatchToProps)(Row);

Great! now after the categories is complete in terminal type ‘npm run dev’ or ‘npm run watch’ and play with category CRUD.

 

Continue to part6: Admin Tags

 

 

0 0 vote
Article Rating
Share this: