React: Contact Detail

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.