官术网_书友最值得收藏!

The Contact module

This module will hold all the necessary files to manage contacts. As we discussed earlier, we are grouping our files by context, related to their domain. The starting point of our module will be the data layer, which means we'll start implementing the necessary service.

Contact service

Our contact service will have basic CRUD operations and Observable streams to subscribe to. This implementation will use the backend API built using Node.js and Express, but it can be converted anytime to a WebSocket-based API with little effort.

Create a new service file called contact-manager/src/contact/contact.service.ts and add the following code:

import { Injectable } from 'angular2/core';
import { Response, Headers } from 'angular2/http';
import { Observable } from 'rxjs/Observable';
import { contentHeaders } from '../common/headers';
import { AuthHttp } from '../auth/auth-http';
import { Contact } from '../contact';

type ObservableContacts = Observable<Array<Contact>>;
type ObservableContact = Observable<Contact>;

const DEFAULT_URL = '/api/contacts';

@Injectable()
export class ContactService {
  public contact: ObservableContact;
  public contacts: ObservableContacts;

  private _authHttp: AuthHttp;
  private _dataStore: { contacts: Array<Contact>, contact: Contact };
  private _contactsObserver: any;
  private _contactObserver: any;
  private _url: string;

  constructor(authHttp: AuthHttp) {
    this._authHttp = authHttp;
    this._url = DEFAULT_URL;
    this._dataStore = { contacts: [], contact: new Contact() };
    this.contacts = new Observable(
      observer => this._contactsObserver = observer
    ).share();
    this.contact = new Observable(
      observer => this._contactObserver = observer
    ).share();
  }
}

In the contact service, we have a few moving parts. First we defined our Observables so that any other component or module can subscribe and start getting the streams of data.

Second, we declared a private data store. This is where we are going to store our contacts. This is good practice as you can easily return all resources from memory.

Also, in our service, we are going to keep private the returned Observers when new instances of Observables are generated. Using the Observers, we can push new data streams to our Observables.

In our public methods, we are going to expose the get all contacts, get one, update, and delete functionalities. To get all contacts, we are going to add the following method to our ContactService:

  public getAll() {
    return this._authHttp
    .get(`${this._url}`, { headers: contentHeaders} )
    .map((res: Response) => res.json())
    .map(data => {
      return data.map(contact => {
        return new Contact(
          contact._id,
          contact.email,
          contact.name,
          contact.city,
          contact.phoneNumber,
          contact.company,
          contact.createdAt
        )
      });
    })
    .subscribe((contacts: Array<Contact>) => {
      this._dataStore.contacts = contacts;
      this._contactsObserver.next(this._dataStore.contacts);
    }, err => console.error(err));
  }

We use our custom build AuthHttp service to load data from our Express application. When a response is received, we transform it into a JSON file, and after that, we just instantiate a new contact for each entity from the dataset.

Instead of returning the whole Observable from the HTTP service, we use our internal data store to persist all the contacts. After we have successfully updated the data store with the new data, we push the changes to our contactsObserver.

Any component that is subscribed to our stream of contacts will get the new values from the Observable data stream. In this way, we always keep our components synced using one single point of entry.

Much of our public method's logic is the same, but we still have a few distinct elements, for example, the update method:

  public update(contact: Contact) {
    return this._authHttp
    .put(
      `${this._url}/${contact._id}`,
      this._serialize(contact),
      { headers: contentHeaders}
    )
    .map((res: Response) => res.json())
    .map(data => {
      return new Contact(
        data._id,
        data.email,
        data.name,
        data.city,
        data.phoneNumber,
        contact.company,
        data.createdAt
      )
    })
    .subscribe((contact: Contact) => {
      // update the current list of contacts
      this._dataStore.contacts.map((c, i) => {
        if (c._id === contact._id) {
          this._dataStore.contacts[i] = contact;
        }
      });
      // update the current contact
      this._dataStore.contact = contact;
      this._contactObserver.next(this._dataStore.contact);
      this._contactsObserver.next(this._dataStore.contacts);
    }, err => console.error(err));
  }

The update method is almost the same as the create() method, however it takes the contact's ID as the URL param. Instead of pushing new values down a data stream, we return the Observable from the Http service, in order to apply operations from the caller module.

Now, if we would like to make changes directly on the datastore and push the new values through the contacts data stream, we could showcase this in the remove contact method:

  public remove(contactId: string) {
    this._authHttp
    .delete(`${this._url}/${contactId}`)
    .subscribe(() => {
      this._dataStore.contacts.map((c, i) => {
        if (c._id === contactId) {
          this._dataStore.contacts.splice(i, 1);
        }
      });
      this._contactsObserver.next(this._dataStore.contacts);
    }, err => console.error(err));
  }

We simply use the map() function to find the contact we deleted and remove it from the internal store. Afterwards, we send new data to the subscribers.

Contact component

As we have moved everything related to the contact domain, we can define a main component in our module. Let's call it contact-manager/public/src/contact/contact.component.ts. Add the following lines of code:

import { Component } from 'angular2/core';
import { RouteConfig, RouterOutlet } from 'angular2/router';
import { ContactListComponent } from './contact-list.component';
import { ContactCreateComponent } from './contact-create.component';
import { ContactEditComponent } from './contact-edit.component';

@RouteConfig([
  { path: '/', as: 'ContactList', component: ContactListComponent, useAsDefault: true },
  { path: '/:id', as: 'ContactEdit', component: ContactEditComponent },
  { path: '/create', as: 'ContactCreate', component: ContactCreateComponent }
])
@Component({
    selector: 'contact',
    directives: [
      ContactListComponent,
      RouterOutlet
    ],
    template: `
      <router-outlet></router-outlet>
    `
})
export class ContactComponent {
  constructor() {}
}

Our component has no logic associated with it, but we used the RouterConfig annotation. The route config decorator takes an array of routes. Each path specified in the config will match the browser's URL. Each route will load the mounted component. In order to reference routes in the template, we need to give them a name.

Now, the most appealing part is that we can take this component with the configured routes and mount it on another component to have Child/Parent routes. In this case, it becomes nested routing, which is a very powerful feature added to Angular 2.

Our application's routes will have a tree-like structure; other components load components with their configured routes. I was pretty amazed by this feature because it enables us to truly modularize our application and create amazing, reusable modules.

List contacts component

In the previous component, we used three different components and mounted them on different routes. We are not going to discuss each of them, so we will choose one. As we have already worked with forms in the Signin component, let's try something different and implement the list contacts functionality.

Create a new file called contact-manager/public/src/contact/contact-list.component.ts and add the following code for your component:

import { Component, OnInit } from 'angular2/core';
import { RouterLink } from 'angular2/router';
import { ContactService } from '../contact.service';
import { Contact } from '../contact';

@Component({
    selector: 'contact-list',
    directives: [RouterLink],
    template: `
      <div class="row">
        <h4>
          Total contacts: <span class="muted">({{contacts.length}})</span>
          <a href="#" [routerLink]="['ContactCreate']">add new</a>
        </h4>
        <div class="contact-list">
          <div class="card-item col col-25 contact-item"
            *ngFor="#contact of contacts">
            <img src="{{ contact.image }}" />
            <h3>
              <a href="#" [routerLink]="['ContactEdit', { id: contact._id }]">
                {{ contact.name }}
              </a>
            </h3>
            <p>
              <span>{{ contact.city }}</span>
              <span>·</span>
              <span>{{ contact.company }}</span>
            </p>
            <p><span>{{ contact.email }}</span></p>
            <p><span>{{ contact.phoneNumber }}</span></p>
          </div>
        </div>
      </div>
    `
})
export class ContactListComponent implements OnInit {
  public contacts: Array<Contact> = [];
  private _contactService: ContactService;

  constructor(contactService: ContactService) {
    this._contactService = contactService;
  }

  ngOnInit() {
    this._contactService.contacts.subscribe(contacts => {
      this.contacts = contacts;
    });
    this._contactService.getAll();
  }
}

In our component's ngOnInit(), we subscribe to the contacts data stream. Afterwards, we retrieve all the contacts from the backend. In the template, we use ngFor to iterate over the dataset and display each contact.

Creating a contact component

Now that we can list contacts in our application, we should also be able to add new entries. Remember that earlier we used the RouterLink to be able to navigate to the CreateContact route.

The preceding route will load the CreateContactComponent, which will enable us to add new contact entries into our database, through the Express API. Let's create a new component file public/src/contact/components/contact-create.component.ts:

import { Component, OnInit } from 'angular2/core';
import { Router, RouterLink } from 'angular2/router';
import { ContactService } from '../contact.service';
import { Contact } from '../contact';

@Component({
    selector: 'contact-create,
    directives: [RouterLink],
    templateUrl: 'src/contact/components/contact-form.html'
})
export class ContactCreateComponent implements OnInit {
  public contact: Contact;
  private _router: Router;
  private _contactService: ContactService;

  constructor(
    contactService: ContactService,
    router: Router
  ) {
    this._contactService = contactService;
    this._router = router;
  }

  ngOnInit() {
    this.contact = new Contact();
  }

  onSubmit(event) {
    event.preventDefault();

    this._contactService
    .create(this.contact)
    .subscribe((contact) => {
      this._router.navigate(['ContactList']);
    }, err => console.error(err));
  }
}

Instead of using an embedded template, we are using an external template file that is configured using the templateUrl property in the component annotation. There are pros and cons for each situation. The benefits of using an external template file would be that you can reuse the same file for more than one component.

The downfall, at the moment of writing the book, in Angular 2 is that it's hard to use relative paths to your template files, so this would make your components less portable. Also I like to keep my templates short, so they can fit easily inside the component, so in most cases I'll probably use embedded templates.

Let's take a look at the template before further discussing the component, public/src/contact/components/contact-form.html:

<div class="row contact-form-wrapper">
  <a href="#" [routerLink]="['ContactList']">&lt; back to contacts</a>
  <h2>Add new contact</h2>
  <form role="form"
    (submit)="onSubmit($event)">

    <div class="form-group">
      <label for="name">Full name</label>
      <input type="text" [(ngModel)]="contact.name"
        class="form-control" id="name" placeholder="Jane Doe">
    </div>
    <div class="form-group">
      <label for="email">E-mail</label>
      <input type="text" [(ngModel)]="contact.email"
        class="form-control" id="email" placeholder="jane.doe@example.com">
    </div>
    <div class="form-group">
      <label for="city">City</label>
      <input type="text"
        [(ngModel)]="contact.city"
        class="form-control" id="city" placeholder="a nice place ...">
    </div>
    <div class="form-group">
      <label for="company">Company</label>
      <input type="text"
        [(ngModel)]="contact.company"
        class="form-control" id="company" placeholder="working at ...">
    </div>
    <div class="form-group">
      <label for="phoneNumber">Phone</label>
      <input type="text"
        [(ngModel)]="contact.phoneNumber"
        class="form-control" id="phoneNumber" placeholder="mobile or landline">
    </div>

    <button type="submit" class="button">Submit</button>
  </form>
</div>

In the template we are using a onSubmit() method from the component to piggyback the form submission and in this case create a new contact and store the data in MongoDB. When we successfully create the contact we want to navigate to the ContactList route.

We are not using local variables, instead we are using two-way data binding with the ngModel for each input, mapped to the properties of the contact object. Now, each time the user changes the inputs value, this is stored in the contact object and on submit it's sent across the wire to the backend.

The RouterLink is used to construct the navigation to the ContactList component from the template. I've left a small improvement, the view title will be the same both for creating and editing, more precisely "Add new contact", and I'll let you figure it out.

Editing an existing contact

When editing a contact, we want to load a specific resource by ID from the backend API and make changes for that contact. Lucky for us this is quite simple to achieve in Angular. Create a new file public/src/contact/components/contact-edit.component.ts:

import { Component, OnInit } from 'angular2/core';
import { RouteParams, RouterLink } from 'angular2/router';
import { ContactService } from '../contact.service';
import { Contact } from '../contact';

@Component({
    selector: 'contact-edit',
    directives: [RouterLink],
    templateUrl: 'src/contact/components/contact-form.html'
})
export class ContactEditComponent implements OnInit {
  public contact: Contact;
  private _contactService: ContactService;
  private _routeParams: RouteParams;

  constructor(
    contactService: ContactService,
    routerParams: RouteParams
  ) {
    this._contactService = contactService;
    this._routeParams = routerParams;
  }

  ngOnInit() {
    const id: string = this._routeParams.get('id');
    this.contact = new Contact();
    this._contactService
    .contact.subscribe((contact) => {
      this.contact = contact;
    });
    this._contactService.getOne(id);
  }

  onSubmit(event) {
    event.preventDefault();

    this._contactService
    .update(this.contact)
    .subscribe((contact) => {
      this.contact = contact;
    }, err => console.error(err));
  }
}

We are not so far away from the ContactCreateComponent, the structure of the class is almost the same. Instead of the Router, we are using RouteParams to load the ID from the URL and retrieve the desired contact from the Express application.

We subscribe to the contact Observable returned by the ContactService to get the new data. In other words our component will react to the data stream and when the data is available it will display it to the user.

When submitting the form, we update the contact persisted in MongoDB and change the view's contact object with the freshly received data from the backend.

主站蜘蛛池模板: 东阿县| 蒙阴县| 赫章县| 上高县| 湘潭市| 山阳县| 曲水县| 黄大仙区| 德州市| 武宁县| 武义县| 连江县| 成安县| 望都县| 洛川县| 若尔盖县| 南召县| 布拖县| 县级市| 南宁市| 甘南县| 碌曲县| 宜城市| 崇礼县| 惠安县| 广东省| 布尔津县| 胶州市| 大荔县| 阜平县| 长岛县| 黔西县| 泰宁县| 岳阳县| 武定县| 和林格尔县| 行唐县| 横峰县| 庆云县| 福州市| 上饶市|