Last week’s post covered adding a React project to the ASP.NET Core Basics solution. As I stated last week 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 read-only view of a contact’s details. The code before any changes can be found here.
Contact Class
The Contact interface in the ContactList.tsx should be deleted and in its please we will add a Contact class. As part of this change, I also moved the contact related items to a contact directory. Add a contact.ts file to the ClientApp/components/contacts/ directory with the following contents.
export class Contact {
id: number;
name: string;
address: string;
city: string;
state: string;
postalCode: string;
phone: string;
email: string;
constructor(data?: any) {
if (data == null) return;
(<any>Object).assign(this, data);
}
getAddress(): string {
return `${this.address} ${this.city}, ${this.state} ${this.postalCode}`;
}
}
This class is very simple and will be used later to show how a function call can be used as part of rendering.
Contact Service
Since the application will now have two places that need to access the ASP.NET Core API I decided to refactor the API access behind a service. This is the same style used by the Aurelia and Angular applications. Add a contactService.ts file to the Contacts directory. The service will provide functions to get all contacts or a single contact using its ID. The following is the full class.
import 'isomorphic-fetch';
import { Contact } from './contact';
export class ContactService {
private baseUrl = 'http://localhost:13322/api/contactsApi/';
getAll(): Promise<Contact[]> {
return fetch(this.baseUrl)
.then(response => response.json() as Promise<Contact[]>)
.then(contacts => Array.from(contacts, c => new Contact(c)));
}
getById(id: string): Promise<Contact> {
return fetch(`${this.baseUrl}${id}`)
.then(response => response.json())
.then(contact => new Contact(contact));
}
}
The final step in this refactor is to use the new service in the ContactList class. First, add imports for the service and Contact class.
import { Contact } from './contact';
import { ContactService } from './contactService';
Then, replace the fetch call with the service.
Before:
fetch('http://localhost:13322/api/contactsApi/')
.then(response => response.json() as Promise<Contact[]>)
After:
let contactService = new ContactService();
contactService.getAll()
Contact Detail Component
Add a ContactDetail.tsx file which will be used to show the details of a contact including using the getAddress function of the Contact class. The following is the full contents of the file.
import * as React from 'react';
import { RouteComponentProps } from 'react-router';
import { Link, NavLink } from 'react-router-dom';
import 'isomorphic-fetch';
import { Contact } from './contact';
import { ContactService } from './contactService';
interface ContactDetailState {
id: string;
contact: Contact | undefined;
loading: boolean;
}
export class ContactDetail extends React.Component<RouteComponentProps<{}>, ContactDetailState> {
constructor(props: any) {
super();
this.state = { id: props.match.params.id, contact: undefined, loading: true };
let contactService = new ContactService();
contactService.getById(this.state.id)
.then(data => {
this.setState({ contact: data, loading: false });
});
}
public render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: this.state.contact
? ContactDetail.renderContactsTable(this.state.contact)
: <p>No contacts</p>;
return <div>
<h1>Contact Detail</h1>
<hr />
{contents}
<NavLink to={'/contactlist'}>Back to List</NavLink>
<hr />
</div>;
}
private static renderContactsTable(contact: Contact) {
return <dl className="dl-horizontal">
<dt>ID</dt>
<dd>{contact.id}</dd>
<dt>Name</dt>
<dd>{contact.name}</dd>
<dt>Address</dt>
<dd>{contact.getAddress()}</dd>
<dt>Phone</dt>
<dd>{contact.phone}</dd>
<dt>Email</dt>
<dd>{contact.email}</dd>
</dl>;
}
}
This is all very similar to the things I have covered before. Now that we have a contact detail component we need a way to display it.
Routing with a Parameter
Import the contact detail in the route.tsx file.
import { ContactDetail } from './components/contacts/ContactDetail';
Next, add a route path for contact detail that expects an ID.
<Route path='/contactdetail/:id' component={ContactDetail} />
Back in the ContactList component change the ID to be a link to the new contact detail route using the ID of the contact.
Before:
<td>{contact.id}</td>
After:
<td><Link to={`contactdetail/${contact.id}`}>{contact.id}</Link></td>
The code for pulling the route parameter was in the ContactDetail component above, but I am going to show it again just so all the route with parameter information is together. The route parameters can be accessed using props.match.params.{parameter name} which in this case ends up being props.match.params.id. The following is the constructor of the ContactDetail component which is using a route parameter.
constructor(props: any) {
super();
this.state = { id: props.match.params.id, contact: undefined, loading: true };
let contactService = new ContactService();
contactService.getById(this.state.id)
.then(data => {
this.setState({ contact: data, loading: false });
});
}
Wrapping Up
This brings the projects one step closer to being on the same level feature-wise. I expect at least one more post to get the project features lined up so make sure and keep a lookout for the next post.
Keep in mind that this is my first look at React and my examples may or may not be idiomatic. So far I am really enjoying working with React.
The code in a finished state can be found here.
Also published on Medium.