- MEAN Blueprints
- Robert Onodi
- 1857字
- 2021-07-16 10:40:20
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']">< 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.
- 極簡算法史:從數學到機器的故事
- Qt 5 and OpenCV 4 Computer Vision Projects
- Python自動化運維快速入門
- 我的第一本算法書
- Visual C++串口通信技術詳解(第2版)
- Apache Karaf Cookbook
- Unreal Engine 4 Shaders and Effects Cookbook
- 計算機應用基礎案例教程
- Python爬蟲、數據分析與可視化:工具詳解與案例實戰
- Bootstrap for Rails
- Flink技術內幕:架構設計與實現原理
- Android移動應用開發項目教程
- 人人都能開發RPA機器人:UiPath從入門到實戰
- 虛擬現實建模與編程(SketchUp+OSG開發技術)
- Web程序設計與架構