Building a Blog With Reactjs And Laravel Part6 Admin Tags

Building a Blog With Reactjs And Laravel Part6: Admin Tags

In this tutorial of the series ‘creating blog with Laravel and Reactjs’ we continue by manipulating tags crud with redux, then connecting this to the UI.

 

 

 

 

The process of manipulating tags is straightforward the same as manipulating categories, we will first create the redux store then update the components but at first let’s create the apis using axios.

Create new file resources/js/admin/apis/Tag.js and add this code:

import axios from 'axios';

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

export default Tag;

The above represent the api needed to do the actual CRUD operations for tags. Next let’s create the redux store for tags.

 

Manipulating Redux Store

Open resources/js/admin/store/actionTypes/TagTypes.js, add the below code:

export const LIST_TAGS = 'LIST_TAGS';
export const LIST_TAGS_SUCCESS = 'LIST_TAGS_SUCCESS';
export const LIST_TAGS_FAILURE = 'LIST_TAGS_FAILURE';
export const CREATE_TAGS = 'CREATE_TAGS';
export const CREATE_TAGS_SUCCESS = 'CREATE_TAGS_SUCCESS';
export const CREATE_TAGS_FAILURE = 'CREATE_TAGS_FAILURE';
export const EDIT_TAGS = 'EDIT_TAGS';
export const EDIT_TAGS_SUCCESS = 'EDIT_TAGS_SUCCESS';
export const EDIT_TAGS_FAILURE = 'EDIT_TAGS_FAILURE';
export const DELETE_TAGS = 'DELETE_TAGS';
export const DELETE_TAGS_SUCCESS = 'DELETE_TAGS_SUCCESS';
export const DELETE_TAGS_FAILURE = 'DELETE_TAGS_FAILURE';
export const SHOW_TAG = 'SHOW_TAG';
export const SHOW_TAG_SUCCESS = 'SHOW_TAG_SUCCESS';
export const SHOW_TAG_FAILURE = 'SHOW_TAG_FAILURE';
export const HANDLE_TAG_TITLE = 'HANDLE_TAG_TITLE';
export const SET_TAG_DEFAULTS = 'SET_TAG_DEFAULTS';
export const LIST_ALL_TAGS = 'LIST_ALL_TAGS';

The above file represent the action types we will use with tags as you learned in previous part when we process categories that we need unique action types defined as constants so that we can update state in the reducer.

Now open resources/js/admin/store/actions/TagActions.js and update it with the below code:

import * as TagTypes from '../actionTypes/TagTypes';

import Tag from '../../apis/Tag';

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

        dispatch({
            type: TagTypes.HANDLE_TAG_TITLE,
            data: title
        });
    }
}

function setTagDefaults() {

    return function (dispatch, getState) {

        dispatch({
            type: TagTypes.SET_TAG_DEFAULTS
        });
    }
}

/**
 * list Tags action
 */
function listTags(page = 1) {

    return function (dispatch, getState) {

        // start sending request (first dispatch)
        dispatch({
            type: TagTypes.LIST_TAGS
        });


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

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

    return function (dispatch, getState) {

        // async call
        Tag.listAll().then(response => {
            dispatch({
                type: TagTypes.LIST_ALL_TAGS,
                data: response.data.data
            });
        });
    }
}

/**
 * add tag action
 */
function addTag (title, cb) {

    return function(dispatch, getState) {

        // start creation show spinner
        dispatch({
            type: TagTypes.CREATE_TAGS
        });

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

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

/**
 * show tag action
 */
function showTag(id)
{
    return function (dispatch, getState) {
        // start creation show spinner
        dispatch({
            type: TagTypes.SHOW_TAG
        });


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

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

/**
 * edit tag action
 */
function editTag(title, id, cb)
{
    return function (dispatch, getState) {
        // start creation show spinner
        dispatch({
            type: TagTypes.EDIT_TAGS
        });


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

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

/**
 * delete tag action
 */
function deleteTag(id)
{
    return function (dispatch, getState) {

        // start creation show spinner
        dispatch({
            type: TagTypes.DELETE_TAGS
        });


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

export {
    listTags,
    handleTagTitle,
    addTag,
    showTag,
    editTag,
    deleteTag,
    setTagDefaults,
    listAllTags
};

In the above code i added all the actions needed like listTags, addTag, editTag, etc. Note that action types and actions is much the same as those in categories.

 

After we defined the actions we need to update the reducer so that it can trigger state updates based on action type. To do this open resources/js/admin/store/reducers/TagReducer.js and update it like this:

import * as TagTypes from '../actionTypes/TagTypes';

const initialState = {
    tags: {},            // used in listing page
    all_tags: [],        // used in fill dropdowns
    tag: {
        id: "",
        title: ""
    },
    success_message: "",
    error_message: "",
    validation_errors: null,
    list_spinner: false,
    create_update_spinner: false
};

const tagReducer = function (state = initialState, action) {
    switch (action.type) {
        case TagTypes.SET_TAG_DEFAULTS:
            return {
                ...state,
                tag: {...state.tag},
                success_message: "",
                error_message: "",
                validation_errors: null,
                list_spinner: false,
                create_update_spinner: false
            };
        case TagTypes.HANDLE_TAG_TITLE:
            return {
                ...state,
                tag: {...state.tag, title: action.data}
            };
        case TagTypes.LIST_TAGS:
            return {
                ...state,
                list_spinner: true
            };
        case TagTypes.LIST_TAGS_SUCCESS:
            return {
                ...state,
                tags: action.data,
                list_spinner: false
            };
        case TagTypes.LIST_TAGS_FAILURE:
            return {
                ...state,
                error_message: action.error,
                list_spinner: false
            };
        case TagTypes.LIST_ALL_TAGS:
            return {
                ...state,
                all_tags: action.data
            };
        case TagTypes.CREATE_TAGS:
            return {
                ...state,
                create_update_spinner: true
            };
        case TagTypes.CREATE_TAGS_SUCCESS:
            return {
                ...state,
                create_update_spinner: false,
                tag: action.data.data,
                success_message: action.data.message,
                error_message: "",
                validation_errors: null
            };
        case TagTypes.CREATE_TAGS_FAILURE:
            return {
                ...state,
                create_update_spinner: false,
                error_message: action.error.message,
                validation_errors: action.error.errors,
                success_message: ""
            };
        case TagTypes.SHOW_TAG:
            return {
                ...state,
                create_update_spinner: true
            };
        case TagTypes.SHOW_TAG_SUCCESS:
            return {
                ...state,
                create_update_spinner: false,
                tag: action.data.data
            };
        case TagTypes.SHOW_TAG_FAILURE:
            return {
                ...state,
                create_update_spinner: false,
                error_message: action.error.message
            };

        case TagTypes.EDIT_TAGS:
            return {
                ...state,
                create_update_spinner: true
            };
        case TagTypes.EDIT_TAGS_SUCCESS:
            return {
                ...state,
                create_update_spinner: false,
                tag: action.data.data,
                success_message: action.data.message,
                error_message: "",
                validation_errors: null
            };
        case TagTypes.EDIT_TAGS_FAILURE:
            return {
                ...state,
                create_update_spinner: false,
                error_message: action.error.message,
                validation_errors: action.error.errors,
                success_message: ""
            };
        case TagTypes.DELETE_TAGS:
            return {
                ...state,
                list_spinner: true
            };
        case TagTypes.DELETE_TAGS_SUCCESS:
            let tags = state.tags;
            tags.data = state.tags.data.filter(item => item.id != action.id);

            return {
                ...state,
                list_spinner: false,
                tags: tags,
                success_message: action.message,
                error_message: ''
            };
        case TagTypes.DELETE_TAGS_FAILURE:
            return {
                ...state,
                list_spinner: false,
                error_message: action.error.message,
                success_message: ''
            };
        default:
            return state;
    }
};

export default tagReducer;

Here as we did with categories by checking for action type in switch statement then updating the related state accordingly.

To finish the final piece of the redux store we need to update the root reducer so that the app can recognize the new state object.

Update resources/js/admin/store/reducers/RootReducer.js as shown:

import { combineReducers } from 'redux';

import categoryReducer  from './CategoryReducer';
import tagReducer  from './TagReducer';

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

export default rootReducer;

Here we added an alias for tag reducer which is tag so i can access the tags in components like this ‘this.props.tag‘.



Updating Components

List All Tags

Open resources/js/admin/components/pages/tags/Index.js and update with this code:

import React from 'react';
import Breadcrumb from '../../partials/Breadcrumb';
import {connect} from 'react-redux';
import { listTags, setTagDefaults } from '../../../store/actions/TagActions';
import Spinner from '../../partials/Spinner';
import { Link } from 'react-router-dom';
import Row from './Row';
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.setTagDefaults();

        this.props.listTags(1);
    }

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

                                    <Link to='tags/add' className="btn btn-primary pull-right">Add <i className="fa fa-plus"></i></Link>
                                </div>
                                <div className="box-body">
                                    <Spinner show={this.props.tag.list_spinner}/>

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

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

                                    </table>
                                </div>

                                <Pagination data={this.props.tag.tags} onclick={this.props.listTags.bind(this)} />
                            </div>
                        </div>
                    </div>
                </section>
            </div>
        );
    }
}

const mapStateToProps = (state, ownProps) => {

    return {
        tag: state.tag
    }
};

const mapDispatchToProps = (dispatch) => {

    return {
        listTags: (page) => dispatch(listTags(page)),
        setTagDefaults: () => dispatch(setTagDefaults())
    }
};

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

resources/js/admin/components/pages/tags/Row.js

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

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

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

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

As you see in the above code i used connect() the utility function in react redux to map state to props and map dispatch to props. Then in the component i can access those as normal props, for example in the componentDidMount() lifecycle hook i called this.props.setTagDefaults() to reset some state variables, add this.props.listTags(1) so that we can fetch the first page of tags from the server.

Also in the render() function we looped tags using javascript map() function which return a new <Row /> component passing in a single tag as a prop. In The <Row /> component we can delete tags by calling this.props.deleteTag().



Creating Tags

To create new tags update resources/js/admin/components/pages/tags/Add.js with this code:

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

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

// actions
import {addTag, setTagDefaults, handleTagTitle} from '../../../store/actions/TagActions';


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

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

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

        this.props.handleTitleChange('');
    }

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

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


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

                                    <Link to='/tags' 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}>

                                    <TagForm tag={this.props.tag} onchange={this.handleChange}/>

                                    <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 {
      tag: state.tag
    };
};

const mapDispatchToProps = (dispatch) => {

    return {
        setTagDefaults: () => dispatch(setTagDefaults()),
        handleTitleChange: (title) => dispatch(handleTagTitle(title)),
        addTag: (title, cb) => dispatch(addTag(title, cb))
    };
};

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



Create this file resources/js/admin/components/pages/tags/TagForm.js and add this code:

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.tag.create_update_spinner}/>
                <SuccessAlert msg={this.props.tag.success_message}/>
                <ErrorAlert msg={this.props.tag.error_message}/>

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

            </div>
        )
    }
}

export default Form;

 

Updating Tags

Open resources/js/admin/components/pages/tags/Edit.js and update it with this code:

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

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

// actions
import {showTag, setTagDefaults, handleTagTitle, editTag} from '../../../store/actions/TagActions';


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

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

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

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

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

    handleChange(e) {
        e.preventDefault();

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

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

        this.props.editTag(this.props.tag.tag.title,
            this.props.match.params.id, function () {

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

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


    render()
    {
        return (
            <div className="content-wrapper">
                <section className="content-header">
                    <h1>
                        Edit tag
                    </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 tag #{ this.props.match.params.id }</h3>

                                    <Link to='/tags' 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}>

                                    <TagForm tag={this.props.tag} onchange={this.handleChange}/>

                                    <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 {
        tag: state.tag
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        showTag: (id) => dispatch(showTag(id)),
        handleTitleChange: (title) => dispatch(handleTagTitle(title)),
        editTag: (title, id, cb) => dispatch(editTag(title, id, cb)),
        setTagDefaults: () => dispatch(setTagDefaults())
    };
};

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

Now let’s try that this is working in terminal run

npm run dev

or 

npm run watch

 

 

Continue to part7: Admin Posts

 

0 0 vote
Article Rating
Share this: