• Tidak ada hasil yang ditemukan

Chapter 9 ■ Forms

As you’d have realized by now, masking the input has its downside. To demonstrate, place the cursor before 3 when the input’s value is 1234. Then, hit any non-numeric key, say, a space. You’ll see that the cursor jumps to the end of the input. This happens when the input’s value is ignored in the onChange event handler.

Why this happens is roughly like this: when a keystroke is received in the input element, the changes first get applied to the value of the input. The actual DOM now has the value, which includes the user’s keystroke. Then, an event is generated, which causes React’s rerendering to kick in, regardless of whether you do a setState. At this point, React compares the input’s value in the virtual DOM with the state variable. If the two are the same, it leaves the input alone. If they are different, it sets the value of the input back to the state variable. Since the input is being set with a new value (the original state value), the cursor moves to the end. Note that the cursor movement is a behavior that browsers implement, with or without React.

There are different approaches to address this issue. One option is to handle the cursor movement on every event. Packages like react-input-mask do this in addition to letting you define masks on the input characters. Another approach is to always let the state variable and the input’s value be synchronized, and do any validation or masking when the input loses focus. We’ll use the latter approach when designing the Date input.

Chapter 9 ■ Forms

181 The code for all this, including error handling, is shown in Listing 9-8.

Listing 9-8. server.js: New Express Route for Get API app.get('/api/issues/:id', (req, res) => { let issueId;

try {

issueId = new ObjectId(req.params.id);

} catch (error) {

res.status(422).json({ message: `Invalid issue ID format: ${error}` });

return;

}

db.collection('issues').find({ _id: issueId }).limit(1) .next()

.then(issue => {

if (!issue) res.status(404).json({ message: `No such issue: ${issueId}` });

else res.json(issue);

})

.catch(error => { console.log(error);

res.status(500).json({ message: `Internal Server Error: ${error}` });

});

});

The code is quite similar to the Create API, except for the parsing and validation of the input parameter for the issue ID. You should be able to test this using curl and get a response like this console output:

$ curl -s http://localhost:8000/api/issues/57e14da9ca2d380662d9d05c | json_pp

{

"effort" : 5, "owner" : "Ravan",

"created" : "2016-08-15T00:00:00.000Z", "status" : "Open",

"_id" : "57e14da9ca2d380662d9d05c",

"title" : "Error in console when clicking Add"

}

You should also test for invalid format of the issue ID (for example with more than 24 characters), and with an issue ID that is valid in format, but does not exist in the database.

Chapter 9 ■ Forms

Edit Page

Now that we have an API to fetch just one issue, let’s use this to render the contents of the Edit page. Let’s create a form to display all the values of the issue, with the editable fields as form inputs. We will not handle the submission of the form yet; we’ll leave that for the following sections.

The render method is simple. We’ll first have two labels for the non-editable fields:

ID and Created Date. As for the rest of the fields, we’ll use a select for the status, and input fields for the rest. Let’s make these controlled components by connecting them to an object in the state called issue. But, instead of handling each input’s onChange individually like we did in IssueFilter, let’s make a common method called onChange.

To differentiate between the inputs that generate the onChange event, we’ll have to supply the name property to each input, which will be set in the event’s target field in the event handler. We’ll use the field’s name as the input name as well. This makes it easy to set the appropriate field of the state object in the event handler: we can just use the target’s name as the key to the state object, and modify its value.

As we did for the IssueList, we’ll add a load() method to fetch the issue properties using the Get API we created in the previous section. We’ll call this method from the componentDidMount() and componentDidUpdate() lifecycle methods.

There is one difference from the IssueList approach. Instead of keeping the field data types as their natural data types, we’ll have to convert them to strings. That’s because the input fields’ value cannot be an object like Date; they can only handle strings.

Finally, we’ll have to create an initial state in the constructor with empty strings. If not, React assumes that the original values were absent, and therefore, that the input fields were uncontrolled components. When the return of the API call sets the issue fields to non-null values, React assumes that we’ve converted an uncontrolled component to a controlled one and issues a warning.

The entire new file is shown in Listing 9-9.

Listing 9-9. IssueEdit.jsx import React from 'react';

import { Link } from 'react-router';

export default class IssueEdit extends React.Component { constructor() {

super();

this.state = { issue: {

_id: '', title: '', status: '', owner: '', effort: '', completionDate: '', created: '',

}, };

this.onChange = this.onChange.bind(this);

}

componentDidMount() { this.loadData();

}

Chapter 9 ■ Forms

183 componentDidUpdate(prevProps) {

if (prevProps.params.id !== this.props.params.id) { this.loadData();

} }

onChange(event) {

const issue = Object.assign({}, this.state.issue);

issue[event.target.name] = event.target.value;

this.setState({ issue });

}

loadData() {

fetch(`/api/issues/${this.props.params.id}`).then(response => { if (response.ok) {

response.json().then(issue => {

issue.created = new Date(issue.created).toDateString();

issue.completionDate = issue.completionDate != null ? new Date(issue.completionDate).toDateString() : '';

issue.effort = issue.effort != null ? issue.effort.toString() : '';

this.setState({ issue });

});

} else {

response.json().then(error => {

alert(`Failed to fetch issue: ${error.message}`);

});

}

}).catch(err => {

alert(`Error in fetching data from server: ${err.message}`);

});

}

render() {

const issue = this.state.issue;

return ( <div>

<form>

ID: {issue._id}

<br />

Created: {issue.created}

<br />

Status: <select name="status" value={issue.status}  onChange={this.onChange}>

<option value="New">New</option>

<option value="Open">Open</option>

<option value="Assigned">Assigned</option>

<option value="Fixed">Fixed</option>

<option value="Verified">Verified</option>

Chapter 9 ■ Forms

<option value="Closed">Closed</option>

</select>

<br />

Owner: <input name="owner" value={issue.owner}  onChange={this.onChange} />

<br />

Effort: <input size={5} name="effort" value={issue.effort}  onChange={this.onChange} />

<br />

Completion Date: <input

name="completionDate" value={issue.completionDate}

onChange={this.onChange}

/>

<br />

Title: <input name="title" size={50} value={issue.title}  onChange={this.onChange} />

<br />

<button type="submit">Submit</button>

<Link to="/issues">Back to issue list</Link>

</form>

</div>

);

} }

IssueEdit.propTypes = {

params: React.PropTypes.object.isRequired, };

Let’s examine some parts of the code in greater detail. The following statement in onChange is interesting:

...

issue[event.target.name] = event.target.value;

...

We used the target’s name as the key in the state object to set the value in the state object. This technique helps us combine all of the inputs’ onChange into one. This works only if we set the name property in the inputs, as shown in the last set of highlights within the render() method. For example, the name of the select component was set to status:

...

Status: <select name="status" value={issue.status}  onChange={this.onChange}>

...

Chapter 9 ■ Forms

185 Also, note that the state variables are not exact copies of the object fields. We had to do some data type conversions after fetching the data, for example:

...

issue.created = new Date(issue.created).toDateString();

...

The above set of changes will result in an Edit page that looks as in Figure 9-2. It’s a good idea now to test it to ensure that all off the inputs are editable and faithfully replicate the user input.

Figure 9-2. Edit page placeholder replaced with a form