Last week’s post covered adding a read-only view of a contact’s details. As before the goal is to get the React project’s features in line with the Aurelia and Angular samples. This week we will be adding a form to all addition of a new contact. The code before any changes can be found here.
Contact List
On the contact list page, we need to add a link to create a contact. This will be added just after the header in the ContactList.tsx file.
<h1>Contact List</h1>
<Link to={'contactdetail'}>Create New Contact</Link>
Routing
In order to stay in line with the other sample, the ContactDetail component is going to be handling both the read-only view and the view to add a contact. This means the ID that is currently part of the contact detail needs to be optional. The following is the change to make ID optional by adding a question mark.
Before:
<Route path='/contactdetail/:id' component={ContactDetail} />
After:
<Route path='/contactdetail/:id?' component={ContactDetail} />
Contact Service
In the ContactService class, we need to add a save function. This new function will make a post request to the contacts API and return the new contact with the ID from the API to the caller.
save(contact: Contact): Promise<Contact> {
return fetch(this.baseUrl,
{
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(contact)
})
.then(response => response.json())
.then(contact => new Contact(contact));
}
Contact Detail
The ContactDetail class is where most of the changes are. I had some trouble getting React’s forms to work directly with the instance of the contact class and ended up just storing the parts of a contact directly in state instead of as an object. I expect this is a failure on my part and not an issue with React. I may revisit this in the future. Below is the new structure of the state used by contact details.
interface ContactDetailState {
id: string;
name: string;
address: string;
city: string;
state: string;
postalCode: string;
phone: string;
email: string;
loading: boolean;
redirect: boolean;
}
Next, the constructor needed to be changed to handle the new state structure and to handle being called without an ID.
constructor(props: any) {
super();
if (props.match.params.id == undefined) {
this.state = {
id: props.match.params.id,
name: '', address: '', city: '',
state: '', postalCode: '', phone: '',
email: '',
loading: false,
redirect: false
};
}
else {
this.state = {
id: props.match.params.id,
name: '', address: '', city: '',
state: '', postalCode: '', phone: '',
email: '',
loading: true,
redirect: false
};
let contactService = new ContactService();
contactService.getById(this.state.id)
.then(data => {
this.setState({
name: data.name, address: data.address, city: data.city,
state: data.state, postalCode: data.postalCode, phone: data.phone,
email: data.email,
loading: false
});
});
}
}
Again, this is way more code than it would be the contact class were being used. Next, the render function needs to be adjusted to render the UI for the read-only view or the contact creation. The decision is based on the ID being set or undefined.
public render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: this.state.id != undefined &&
!this.state.redirect
? this.renderExistingContact()
: this.renderNewContact();
return <div>
<h1>Contact Detail</h1>
<hr />
{contents}
<NavLink to={'/contactlist'}>Back to List</NavLink>
<hr />
</div>;
}
The render of an existing contact needs to be changed to use state instead of an instance of a contact.
private renderExistingContact() {
return <dl className="dl-horizontal">
<dt>ID</dt>
<dd>{this.state.id}</dd>
<dt>Name</dt>
<dd>{this.state.name}</dd>
<dt>Address</dt>
<dd>{this.state.address} {this.state.city}, {this.state.state} {this.state.postalCode}</dd>
<dt>Phone</dt>
<dd>{this.state.phone}</dd>
<dt>Email</dt>
<dd>{this.state.email}</dd>
</dl>;
}
The following is the render of the add contact UI. I will call out a couple of parts after. The bulk of the code is just rending of the form.
private renderNewContact() {
return (
<div>
{this.state.redirect && <Redirect to={`/contactdetail/${this.state.id}`}/>}
<form role="form" className="form-horizontal" onSubmit={(e: any) => this.handleSubmit(e)}>
<div className="form-group">
<label className="col-sm-2 control-label">Name</label>
<div className="col-sm-10">
<input type="text" placeholder="name" className="form-control" name="name" value={this.state.name} onChange={(e: any) => this.handleChange(e)} />
</div>
</div>
<div className="form-group">
<label className="col-sm-2 control-label">Address</label>
<div className="col-sm-10">
<input type="text" placeholder="address" className="form-control" name="address" value={this.state.address} onChange={(e: any) => this.handleChange(e)} />
</div>
</div>
<div className="form-group">
<label className="col-sm-2 control-label">City</label>
<div className="col-sm-10">
<input type="text" placeholder="city" className="form-control" name="city" value={this.state.city} onChange={(e: any) => this.handleChange(e)} />
</div>
</div>
<div className="form-group">
<label className="col-sm-2 control-label">State</label>
<div className="col-sm-10">
<input type="text" placeholder="state" className="form-control" name="state" value={this.state.state} onChange={(e: any) => this.handleChange(e)} />
</div>
</div>
<div className="form-group">
<label className="col-sm-2 control-label">Zip</label>
<div className="col-sm-10">
<input type="text" placeholder="zip" className="form-control" name="postalCode" value={this.state.postalCode} onChange={(e: any) => this.handleChange(e)} />
</div>
</div>
<div className="form-group">
<label className="col-sm-2 control-label">Phone</label>
<div className="col-sm-10">
<input type="text" placeholder="phone" className="form-control" name="phone" value={this.state.phone} onChange={(e: any) => this.handleChange(e)} />
</div>
</div>
<div className="form-group">
<label className="col-sm-2 control-label">Email</label>
<div className="col-sm-10">
<input type="email" placeholder="email" className="form-control" name="email" value={this.state.email} onChange={(e: any) => this.handleChange(e)} />
</div>
</div>
<div className="text-center">
<button className="btn btn-success btn-lg" type="submit">Save</button>
<button className="btn btn-danger btn-lg" onClick={() => this.reset()}>Reset</button>
</div >
</form>
</div>
);
}
The following is the input for the contact’s name.
<input type="text" placeholder="name" className="form-control" name="name" value={this.state.name} onChange={(e: any) => this.handleChange(e)} />
Since I decided to go with the controlled component route React will be responsible for being the source of truth, not the form its self. To accomplish this it is important that the input has a name and an onChange event handler set up. The following is the handleChange function which uses the name from the on change event to update the proper property in the component’s state.
private handleChange(event: any): void {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({ [name]: value });
}
The following line is the reset button which will reset the state to blank out all the fields in the form.
<button className=”btn btn-danger btn-lg” onClick={() => this.reset()}>Reset</button>
The reset function just uses setState to blank out all the fields.
private reset() {
this.setState({
name: '', address: '', city: '',
state: '', postalCode: '', phone: '',
email: ''
});
}
Not surprisingly the submit button triggers a submit of the form. What happens on submit is defined in the opening form tag.
<form role="form" className="form-horizontal" onSubmit={(e: any) => this.handleSubmit(e)}>
The handleSubmit function takes the contact information in state and uses it to create a new instance of a Contact which is then passed to the ContactService to be saved. The service returns a new contact object from the server and the ID is stored to state.
handleSubmit(event: any): void {
event.preventDefault();
let contact = new Contact();
contact.name = this.state.name;
contact.address = this.state.address;
contact.city = this.state.city;
contact.state = this.state.state;
contact.postalCode = this.state.postalCode;
contact.phone = this.state.phone;
contact.email = this.state.email;
let contactService = new ContactService();
contactService.save(contact)
.then(c => this.setState({ id: String(c.id) }));
if (this.state.id) {
this.setState({ redirect: true });
}
}
If the server does return an ID for the new contact then the redirect is set to true. Then will case the following code to run in renderNewContact which will redirect the user back to the read-only view of the new contact.
{this.state.redirect && <Redirect to={`/contactdetail/${this.state.id}`}/>}
Wrapping Up
This pretty much gets the React application in line with the Aurelia and Angular sample applications. It has been fun getting a handle on the very, very basics of React. While I am back in the basics sample projects I may go ahead and tackle a Vue sample next.
The finished code can be found here.