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.