Task Tracker - Part 3 - Forms + Sessions

October 18, 2018

In the first two parts of this series, we setup our app, made it so we can move from page to page. We then added UI features and made it so that we could restrict access to some of the “pages” we created using variables we can now carry without us around the application.

Now it is time to bring this application alive.  Please note that this tutorial works in conjunction with my “Basic RESTful API” tutorial in the PHP section.  That tutorial will go over setting up your MySQL database and coding the RESTful API that our React forms and “pages” will interact with.

Talking with the RESTful API

Our React application now needs to work with our RESTful API, so the first thing we need to do is create our RestCall function.  We’re going to separate this from everything else and add it to a different directory that we will call “services”.

In this file we’re going to add the following:

export function RestCall(type, requestData) {

    //let BaseURL = 'https://www.marcusgee.com/demos/task-manager/';
    //let BaseURL = 'https://www.lessonstutorials.com/demo/task-tracker-part-3/api/';
    let BaseURL = '/demo/task-tracker-part-3/api/';

    var formBody = [];
    for (var property in requestData) {
        var encodedKey = encodeURIComponent(property);
        var encodedValue = encodeURIComponent(requestData[property]);
        formBody.push(encodedKey + "=" + encodedValue);
    }
    formBody = formBody.join("&");

    return new Promise((resolve, reject) =>{

        fetch(BaseURL+type, {
            method: 'post',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: formBody
        })
            //.then((response) => { console.log(response);} )

        .then((response) => response.json())
        .then((res) => {
            resolve(res);
        })

        .catch((error) => {
            reject(error);
        });


    });

}

I think the code in this file is straight forward. 

The “baseURL” is going to point at the API location, for example this could be “http://mydomain.com/api”.

I had some issues getting the server to read the “post”, I did a lot of testing and found that the following code fixed the problem, by encoding and formatting the post data appropriately:

    var formBody = [];
    for (var property in requestData) {
        var encodedKey = encodeURIComponent(property);
        var encodedValue = encodeURIComponent(requestData[property]);
        formBody.push(encodedKey + "=" + encodedValue);
    }
    formBody = formBody.join("&");

Then we do “fetch” to the “BaseURL” with the “type” which is the ACTION we are requesting, such as “signin”, “signout”, etc.

We specifically declare the method, headers and we add our post values to the body.

Then it gets sent and we wait for the response, acting accordingly.

Lets get our forms working!

So now we have a service that will allow us to connect with our RESTful API, now it is time to build our forms and get them talking with the server!  We have three forms that we are going to be working with here: “Sign-in”, “register” and “add task”. However we’re going to change up our plans for the “add task” form and instead of creating a separate “page” for it, we are going to pop it up in a Bootstrap modal!

Let us start with Register.js:

The first thing we are going to want to do is to act our RestCall services to our imports.

import {RestCall} from '../../services/RestCall';

Next we are going to add a constructor to our component and in this constructor we are going to set some of our “state” variables as well as add some bindings to handle form field changes and submission clicks.

constructor(props) {
    super(props);

    this.state = {
        username: '',
        usernameErr: false,
        password: '',
        passwordErr: false,
    };

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

}

Now I am going to add a function that will handle field changes, without this function you can type in your fields all you want and nothing will show up.  We could do some as-you-type error checking here, but I’m not going to do that:

handleChange(event) {

    const target = event.target;
    const value = target.value;
    const name = target.name;

    this.setState({
        [name]: value
    });

}

Next we want to handle the submit and call our RestCall services.

handleSubmit(event) {

    event.preventDefault();

    if( !this.checkForErrors(this.state.username,this.state.password) ) {

        RestCall('register', this.state).then((result) => {

            var responseJson = result;

            console.log(responseJson);

            if (responseJson.responseCode === 200) {

                if( responseJson.response.status === 'error' )
                {
                    this.checkForErrors(responseJson.response.username, responseJson.response.password);
                }
                else
                {
                    localStorage.setItem("session_ref",responseJson.response.session_ref);
                    localStorage.setItem("username",responseJson.response.username);
                    this.props.history.push('/dashboard');
                }

            }
            else {
                console.log('response code not 200');
            }

        });

    }

}

To explain some of the above code.  We are disabling the default submission action, because we want to handle the submission after we get a response back from the server.  Next, we are going to check for errors, that code is the next chunk below.  If there are no error messages, we are going to send our form data to the API.

I believe that you should NEVER trust the client, so I do the same error checks on the server side, so when we get our response if errors were found that trigger the error messages using our error function.  Or if there are no errors, I save our session data to local storage and redirect the user.

Note that I save the session reference, this is a 64-character string that means absolutely nothing to anyone, it just references a row in the session table on the server which holds all the user details.

So here is the error checking function I mentioned above:

checkForErrors(username,password)
{

    // Reset Errors

    let error = false;

    this.setState({usernameErr:false});
    this.setState({passwordErr:false});

    //

    if( !username || username === 'username_not_found' )
    {
        error = true;
        this.setState({usernameErr:true});
    }
    else if( !username.match("^[A-z0-9]+$") || username === 'username_invalid_characters' )
    {
        error = true;
        this.setState({usernameErr:true});
    }

    if( !password || password === 'password_not_found' )
    {
        error = true;
        this.setState({passwordErr:true});
    }
    else if( !password.match("^[A-z0-9]+$") || password === 'password_invalid_characters' )
    {
        error = true;
        this.setState({passwordErr:true});
    }

    //

    return error;

}

We reset our errors so that if we still have an error at least those fields that have been cleared no longer show an error.

Finally we have to update the render section to show a form to the visitor:

<div className="row">
    <div className="col-md-6">
        <h2>Register</h2>
    </div>
    <div className="col-md-6">

        <form onSubmit={this.handleSubmit}>
            <div className={this.state.usernameErr?'form-group has-error':'form-group '}>
                <label htmlFor="username">Username</label>
                <input type="text" className="form-control" id="username" name="username" aria-describedby="usernameHelp" placeholder="Username" value={this.state.username} onChange={this.handleChange} />
                <small id="usernameHelp" className="form-text text-muted">Username can only contain letters and numbers.</small>
            </div>
            <div className={this.state.passwordErr?'form-group has-error':'form-group '}>
                <label htmlFor="password">Password</label>
                <input type="password" className="form-control" id="password" name="password" aria-describedby="passwordHelp" placeholder="Password" value={this.state.password} onChange={this.handleChange} />
                <small id="passwordHelp" className="form-text text-muted">Password can only contain letters and numbers.</small>
            </div>

            <button type="submit" className="btn btn-primary">Register</button> <a href="/demo/task-tracker-part-3/" className="btn btn-secondary">Return to Sign-In</a>
        </form>

    </div>
</div>

Above you will notice we have a mixture of Bootstrap UI code and React logic code.

Some of the key aspects are:

<form onSubmit={this.handleSubmit}>

This tells our form to utilize our “handleSubmit” function when someone submits the form.

<div className={this.state.usernameErr?'form-group has-error':'form-group '}>

This code is what hits our “handleChange” function, where we could do additional checking, before sending it back down and filling out the value before our eyes.

That is basically it, and our Signin.js uses basically the same code with a few adjustments.  We change the RestCall function to point to “signin” instead of “register” and upon success we set the session and redirect the visitor.

The complex form.

With our Signin.js and Register.js forms out of the way and with us able to register and sign-in without being booted out, now it is time to move on to add tasks.  This form is a lot more complex than the previous forms.  For this example, I placed the “Add Task” into the Header.js file.

There are two components we want to add to our form, the ability to add tags and the ability to select a date.  Not to mention that we also want our form in a modal this time and not directly outputted to the page.  But first lets get the first to NPM packages installed for tags and date selection.

npm install --save react-tag-input
npm install react-datepicker –save
npm install --save moment react-moment

Now that those are installed we can start updating the Header.js file.  First we have to make some modifications to the top of the file:

import { Modal, Button } from 'react-bootstrap';

import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';

import moment from 'moment';

import { WithContext as ReactTags } from 'react-tag-input';
import {RestCall} from "../../services/RestCall";

const KeyCodes = {
    comma: 188,
    enter: 13,
}; 

const delimiters = [KeyCodes.comma, KeyCodes.enter];

First we add a number of imports...

Modal and Button from the react-bootstrap package.

DatePicker from the react-datepicker package, including the CSS file as well.

Moment from the moment package.

ReactTags from the react-tag-input package.  With the tagging functionality we also do some character mapping.

We also import our RestCall function to communicate with the API.

constructor(props, context) {
    super(props, context);

    this.handleShow = this.handleShow.bind(this);
    this.handleClose = this.handleClose.bind(this);

    this.handleDelete = this.handleDelete.bind(this);
    this.handleAddition = this.handleAddition.bind(this);
    this.handleDrag = this.handleDrag.bind(this);

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

    this.state = {
        show: false,

        session_ref: localStorage.getItem("session_ref"),

        title: '',
        titleErr: '',
        desc: '',
        descErr: '',
        pointsHours: '2',
        priority: 'low',
        assign: '',
        dueDate: moment(),
        tagsFinal: '',
        assignFinal: '',

        names: [
            { name: 'One', id: 1 },
            { name: 'Two', id: 2 },
            { name: 'Three', id: 3 },
            { name: 'four', id: 4 }
        ],

        tags: [
            //{ id: "Thailand", text: "Thailand" },
        ],
        suggestions: [
            //{ id: 'USA', text: 'USA' },
        ]
    };
}

We must add a lot more to this constructor over the previous constructors.

First, we a have several additional handlers to bind.  Then with “state” we must add additional variables.  You notice that I’ve included some temp names, these can be removed since the array is refreshed before the modal is launched.

handleSubmit(event) {

    event.preventDefault();

    if( !this.checkForErrors(this.state.title,this.state.desc) ) {

        //

        RestCall('task', this.state).then((result) => {

            var responseJson = result;

            if (responseJson.responseCode === 200) {

                if( responseJson.response.status === 'error' )
                {
                    this.checkForErrors(responseJson.response.title, responseJson.response.desc);
                }
                else
                {
                    this.props.testfunc();
                    this.setState({ show: false });
                }

            }
            else {
                //console.log('response code not 200');
            }

        });

    }

}

This is basically just like our previous submit functions, of course we are requesting “task” instead of registering or signing-in.  We also close the modal upon success after refreshing the tasks on the dashboard via the “this.props.testfunc();” command.

checkForErrors(title,desc)
{

    // Reset Errors

    let error = false;

    this.setState({titleErr:false});
    this.setState({descErr:false});

    //

    if( !title || title === 'title_not_found' )
    {
        error = true;
        this.setState({titleErr:true});
    }

    if( !desc || desc === 'desc_not_found' )
    {
        error = true;
        this.setState({descErr:true});
    }

    //

    return error;

}

I’ve kept errors to a minimum here, most of the other fields are preset to either already have something select, or not require anything to be submitted.

handleDelete(i) {
    const { tags } = this.state;
    this.setState({
        tags: tags.filter((tag, index) => index !== i),
    }, () => {
        this.setState({ tagsFinal: JSON.stringify(this.state.tags) });
    });
}

handleAddition(tag) {
    this.setState(state => ({ tags: [...state.tags, tag] }), () => {
        this.setState({ tagsFinal: JSON.stringify(this.state.tags) });
    });
}

handleDrag(tag, currPos, newPos) {
    const tags = [...this.state.tags];
    const newTags = tags.slice();

    newTags.splice(currPos, 1);
    newTags.splice(newPos, 0, tag);

    // re-render
    this.setState({ tags: newTags });
}

These functions all deal with the tag features.  As this was a simple straight forward example, the code here was taken directly from the tag package website.

handleChange(event) {

    //this.setState({value: event.target.value});

    const target = event.target;
    const value = target.value;
    const name = target.name;

    this.setState({
        [name]: value
    });

}

This function is the same as our previous “handleChange” functions and our only associated with the forms text fields.

handleDate(date) {

    this.setState({
        dueDate: date
    });

}

I was unable to make the pickdate component work with the “handleChange” function, the variables didn’t line up nicely, however the above function worked like a charm.

handleClose() {
    this.setState({ show: false });
}

handleShow() {

    this.setState({assign:''});

    RestCall('userlist', localStorage.getItem("session_ref")).then((result) => {

        var responseJson = result;

        if (responseJson.responseCode === 200) {

            if( responseJson.response.status === 'error' )
            {
                //console.log('there was an error getting userlist');
            }
            else
            {
                this.setState({ assign: responseJson.response.users[0].id });
                this.setState({ names: responseJson.response.users });
                this.setState({ show: true });
            }

        }
        else {
            //console.log('response code not 200');
        }

    });

}

“handleClose” and “handleShow” deal with the modal.  Nothing much to say about the close function, but with the open function you’ll notice that I call the API to pull a user list which I then use to populate the users array after which I show the modal.

let optionTemplate = [];
for (var k = 0; k < this.state.names.length; k++) {
    optionTemplate.push(<option key={this.state.names[k].id} value={this.state.names[k].id}> {this.state.names[k].name} </option>);
}

var username = localStorage.getItem("username");

The code above handles loading the assigned array with users.

<div>
    <div className="row">
        <div className="col-md-6">Dashboard Component</div>
        <div className="col-md-6"><div className="pull-right">Hello <b>{username}</b> / <a href="//:0" onClick={this.handleShow}>Add Task</a> / <a href="/demo/task-tracker-part-3/signout">Sign Out</a></div></div>
    </div>

    <Modal show={this.state.show} onHide={this.handleClose}>
        <form onSubmit={this.handleSubmit}>
        <Modal.Header closeButton>
            <Modal.Title>Add Task</Modal.Title>
        </Modal.Header>
        <Modal.Body>
            <h4>Text in a modal</h4>
            <p>
                Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
            </p>

            <hr />

            <div className={this.state.titleErr?'form-group has-error':'form-group '}>
                <label htmlFor="title">Title</label>
                <input type="text" className="form-control" id="title" name="title" aria-describedby="titleHelp" placeholder="Task Title" value={this.state.title} onChange={this.handleChange} />
                <small id="titleHelp" className="form-text text-muted">Username can only contain letters and numbers.</small>
            </div>

            <div className={this.state.descErr?'form-group has-error':'form-group '}>
                <label htmlFor="desc">Description</label>
                <textarea className="form-control" id="desc" name="desc" aria-describedby="descHelp" placeholder="My task description" value={this.state.desc} onChange={this.handleChange} />
                <small id="descHelp" className="form-text text-muted">Password can only contain letters and numbers.</small>
            </div>

            <div className="form-group">
                <label htmlFor="pointsHours">Estimated Points / Hours</label>
                <select className="form-control" id="pointsHours" name="pointsHours" value={this.state.pointsHours} onChange={this.handleChange}>
                    <option value="1">30 min / 1 pt</option>
                    <option value="2">1 hr / 2 pts</option>
                    <option value="4">2 hr / 4 pts</option>
                    <option value="16">1 day / 16 pts</option>
                    <option value="32">2 days / 32 pts</option>
                </select>
            </div>

            <div className="form-group">
                <label htmlFor="username">Tags</label>
                <ReactTags tags={this.state.tags}
                           suggestions={this.state.suggestions}
                           handleDelete={this.handleDelete}
                           handleAddition={this.handleAddition}
                           handleDrag={this.handleDrag}
                           delimiters={delimiters} />
            </div>

            <div className="form-group">
                <label htmlFor="priority">Priority</label>
                <select className="form-control" id="priority" name="priority" value={this.state.priority} onChange={this.handleChange}>
                    <option value="high">High</option>
                    <option value="medium">Medium</option>
                    <option value="low">Low</option>
                </select>
            </div>

            <div className="form-group">
                <label htmlFor="assign">Assign to User</label>
                <select className="form-control" id="assign" name="assign" value={this.state.assign} onChange={this.handleChange}>
                    {optionTemplate}
                </select>
            </div>

            <div className="form-group">
                <label htmlFor="dueDate">Due Date</label>
                <DatePicker selected={this.state.dueDate} id="dueDate" onChange={this.handleDate}/>
            </div>
            
        </Modal.Body>
        <Modal.Footer>
            <button type="submit" className="btn btn-primary">Save Task</button> <Button onClick={this.handleClose} className="btn btn-secondary">Cancel</Button>
        </Modal.Footer>
        </form>
    </Modal>

</div>

<Modal show={this.state.show} onHide={this.handleClose}>

Here you can see how we are using “state” to control the whether the modal is showing or not.

<ReactTags tags={this.state.tags}
           suggestions={this.state.suggestions}
           handleDelete={this.handleDelete}
           handleAddition={this.handleAddition}
           handleDrag={this.handleDrag}
           delimiters={delimiters} />

Above is the form field for handling the tags, as you can see it references a number of the handlers we created above as well as pulling the existing tags from the components state.

<DatePicker selected={this.state.dueDate} id="dueDate" onChange={this.handleDate}/>

Unlike the tags field, the DatePicker field is more like the input field.

Lastly we want to show the tasks

For this we are going to use another NPM package, react-trello to again speed up development and because I don’t believe in reinventing the wheel if we don’t have to.

First we need to install the package:

npm install --save react-trello

Next we need to update the Dashboard.js page…

Like other packages we need to import the “react-trello” package.

import Board from 'react-trello'

Next we need to setup the lanes in our constructor, I’ve setup a “To Do”, “In Progress” and “Done” lane.  You can easily add and remove those as you need.

constructor(props) {
    super(props);

    this.state = {
        tasks: {
            lanes: [
                {
                    id: 'todo',
                    title: 'To Do',
                    cards: []
                },
                {
                    id: 'inprogress',
                    title: 'In Progress',
                    cards: []
                },
                {
                    id: 'done',
                    title: 'Done',
                    cards: []
                }
            ]
        }
    };

    this.getTasks();

}

Part of this page needs to pull existing tasks from the API, so I created the following function which works very much like all our other API call functions:

getTasks() {

    console.log(this.state.tasks);
    RestCall('tasks', localStorage.getItem("session_ref")).then((result) => {

        console.log('get tasks 2');
        var responseJson = result;

        console.log(responseJson);

        if (responseJson.responseCode === 200) {

            if (responseJson.response.status === 'error') {
                console.log('there was an error getting tasks');
            }
            else {
                console.log('task stuff');
                console.log(responseJson.response.tasks);
                this.setState({tasks: responseJson.response.tasks});
            }

        }
        else {
            console.log('response code not 200');
        }

    });

}

The react-trello package comes with a “default” card, that is okay.  However, I wanted to create a custom card for this project, so using the following code inside the render function created my custom card:

const CustomCard = props => {
    return (
        <div>
            <header
                style={{
                    borderBottom: '1px solid #eee',
                    paddingBottom: 6,
                    padding: 10,
                    marginBottom: 10,
                    display: 'flex',
                    flexDirection: 'row',
                    justifyContent: 'space-between',
                    color: props.cardColor
                }}>
                <div style={{fontSize: 14, fontWeight: 'bold'}}>{props.priority}</div>
                <div style={{fontSize: 11}}>{props.dueOn}</div>
            </header>
            <div style={{fontSize: 12, padding: 10, color: '#BD3B36'}}>
                <div style={{color: '#4C4C4C', fontWeight: 'bold'}}>{props.title}</div>
                <div style={{color: '#4C4C4C', padding: '5px 0px'}}>
                    <i>{props.desc}</i>
                </div>
                <div style={{color: '#4C4C4C'}}>{props.tags}</div>
                <div style={{color: '#4C4C4C'}}>Assigned To: <b>{props.assigned}</b></div>
            </div>
        </div>
    )
}

Then I created my board and showed my custom cards right under the header.

<Board data={this.state.tasks} draggable customCardLayout handleDragEnd={()=>{this.handleDragEnd()}}>
    <CustomCard />
</Board>

Conclusion

So that concludes my 3 part series on creating a very basic react application.  We covered many subjects from setting up your application, adding a router to move between components, adding Bootstrap for our UI, storing data locally, adding form functionality including error checking, creating a user system and having the ability to add and display tasks.