As you see, our app looks far from fancy using only pure HTML templates. It urgently needs graphical design improvements to be usable. We are going to fix this issue in the next three steps starting from the current. Ultimately, we'll try out two graphical design front-end libraries: Twitter's Bootstrap and Google's Material Design, which are among most popular design libraries and has multiple open-source implementations on the market.
But first up, we'll add one more visual feature to the Socially: maps. This will be quite beneficial, taking into account specifics of our app: parties need precise locations to avoid confusions. We are going to imploy this package of Google Maps components for Angular 2.
Having maps on board, we can make use of latitute and longitute coordinates, which are most precise location information possible. And of course, we'll show everything on the map to make this information comprehensive for users.
There are two pages in the app which will be changed: main page to show all parties' locations on the map and party details page to show and change a particular party's location on the map. If it's done nicely, it will certantly look terrific and attract more users to the app.
Before we start with the maps directly, we need to make some preparations.
First up, we need to extend Party
's with two more properties: "lat" and "lng",
which are the above mentioned latitude and longitude.
Since we have the location name and would like not to remove it since it still might be useful,
let's consider converting "location" property to an object with three properties: "name", "lat", and "lng".
It will require, though, some changes in other parts of the app, where Party
type is used.
Let's add those changes consequently, starting from Party
type itself:
3
4
5
6
7
8
9
13
14
15
16
17
18
19
20
21
22
export interface Party extends CollectionObject {
name: string;
description: string;
location: Location;
owner?: string;
public: boolean;
invited?: string[];
...some lines skipped...
interface RSVP {
userId: string;
response: string;
}
interface Location {
name: string;
lat?: number;
lng?: number;
}
Then, change the parties, that are created and added on the server initially, accordingly:
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const parties: Party[] = [{
name: 'Dubstep-Free Zone',
description: 'Can we please just for an evening not listen to dubstep.',
location: {
name: 'Palo Alto'
},
public: true
}, {
name: 'All dubstep all the time',
description: 'Get it on!',
location: {
name: 'Palo Alto'
},
public: true
}, {
name: 'Savage lounging',
description: 'Leisure suit required. And only fiercest manners.',
location: {
name: 'San Francisco'
},
public: false
}];
The PartiesForm component needs to be changed too to reflect type changes:
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
}
if (this.addForm.valid) {
Parties.insert({
name: this.addForm.value.name,
description: this.addForm.value.description,
location: {
name: this.addForm.value.location
},
public: this.addForm.value.public,
owner: Meteor.userId()
});
this.addForm.reset();
}
Lastly, we are updating the parties publications. It's interesting to see what a small change is required to update the parties search by location: it needs only to point out that "location" property has been moved to "location.name", thanks to Mongo's flexible API:
60
61
62
63
64
65
66
return {
$and: [{
'location.name': searchRegEx
},
isAvailable
]
And also let's update it in the view:
6
7
8
9
10
11
12
<input [disabled]="!isOwner" type="text" [(ngModel)]="party.description" name="description">
<label>Location</label>
<input [disabled]="!isOwner" type="text" [(ngModel)]="party.location.name" name="location">
<button [disabled]="!isOwner" type="submit">Save</button>
<a [routerLink]="['/']">Cancel</a>
Now when we are done with updates, let's reset the database in case it has
parties of the old type (remember how to do it? Execute meteor reset
). Then, run the app to make sure that everything is alright and
works as before.
Now is the time to upgrade above mentioned components to feature Google Maps. Let's add a Meteor package that wraps around that Google Maps NPM package:
$ meteor npm install angular2-google-maps --save
And just like any other external package, we need to import the module into our NgModule
:
4
5
6
7
8
9
10
18
19
20
21
22
23
24
25
26
27
import { RouterModule } from '@angular/router';
import { AccountsModule } from 'angular2-meteor-accounts-ui';
import { Ng2PaginationModule } from 'ng2-pagination';
import { AgmCoreModule } from 'angular2-google-maps/core';
import { AppComponent } from './app.component';
import { routes, ROUTES_PROVIDERS } from './app.routes';
...some lines skipped...
ReactiveFormsModule,
RouterModule.forRoot(routes),
AccountsModule,
Ng2PaginationModule,
AgmCoreModule.forRoot({
apiKey: 'AIzaSyAWoBdZHCNh5R-hB5S5ZZ2oeoYyfdDgniA'
})
],
declarations: [
AppComponent,
The maps package contains two major directives: one is to render a HTML container with Google Maps, another one is to visualize a map marker. Let's add a maps markup to the PartyDetails component's template:
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<input type="button" value="Maybe" (click)="reply('maybe')">
<input type="button" value="No" (click)="reply('no')">
</div>
<sebm-google-map
[latitude]="lat || centerLat"
[longitude]="lng || centerLng"
[zoom]="8"
(mapClick)="mapClicked($event)">
<sebm-google-map-marker
*ngIf="lat && lng"
[latitude]="lat"
[longitude]="lng">
</sebm-google-map-marker>
</sebm-google-map>
It needs some explanation. Our markup now contains these two directives. As you can see, parent map container directive has a party marker directive as a child element, so that it can be parsed and rendered on the map. We are setting "latitude" and "longitude" on the map directive, which will fixate the map at a particular location on the page load.
You may notice as well, four new properties were added: "lat", "lng", "centerLat", and "centerLng". "lat" and "lng" are wrappers over a party's coordinates, while "centerLat" and "centerLng" are default center coordinates. In addition, location property binding has been corrected to reflect new type changes.
Here come changes to the component itself, including imports, new coordinates properties, and maps click event handler:
5
6
7
8
9
10
11
29
30
31
32
33
34
35
36
37
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { Meteor } from 'meteor/meteor';
import { MeteorObservable } from 'meteor-rxjs';
import { InjectUser } from "angular2-meteor-accounts-ui";
import { MouseEvent } from "angular2-google-maps/core";
import 'rxjs/add/operator/map';
...some lines skipped...
users: Observable<User>;
uninvitedSub: Subscription;
user: Meteor.User;
// Default center Palo Alto coordinates.
centerLat: number = 37.4292;
centerLng: number = -122.1381;
constructor(
private route: ActivatedRoute
...some lines skipped...
return false;
}
get lat(): number {
return this.party && this.party.location.lat;
}
get lng(): number {
return this.party && this.party.location.lng;
}
mapClicked($event: MouseEvent) {
this.party.location.lat = $event.coords.lat;
this.party.location.lng = $event.coords.lng;
}
ngOnDestroy() {
this.paramsSub.unsubscribe();
this.partySub.unsubscribe();
It's going to work in a scenario as follows:
That's almost it with the party details. So far, so good and simple.
The last change will be about CSS styles. To show the map container of a specific size, we'll have to set element styles. Since we'll need styles for that for two pages, let's create a separate CSS file for the whole app, which is, anyways, will be useful on the next steps:
1
2
3
4
.sebm-google-map-container {
width: 400px;
height: 400px;
}
As usual, having introduced new feature, we are finishing it up with testing. Let's create a new party with the location name set to some existing place you know, and go to the details page. Click on the maps at the location that corresponds to the party's location name: a new marker should appear there. Now, click "Save" button and re-load the page: it should be loaded with the map and a marker on it at the point you've just pointed out.
Adding multiple markers on the parties front page should be straightforward now. Here is the markup:
18
19
20
21
22
23
24
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<li *ngFor="let party of parties | async">
<a [routerLink]="['/party', party._id]">{{party.name}}</a>
<p>{{party.description}}</p>
<p>{{party.location.name}}</p>
<button [hidden]="!isOwner(party)" (click)="removeParty(party)">X</button>
<div>
Who is coming:
...some lines skipped...
</li>
</ul>
<sebm-google-map
[latitude]="0"
[longitude]="0"
[zoom]="1">
<div *ngFor="let party of parties | async">
<sebm-google-map-marker
*ngIf="party.location.lat"
[latitude]="party.location.lat"
[longitude]="party.location.lng">
</sebm-google-map-marker>
</div>
</sebm-google-map>
<pagination-controls (pageChange)="onPageChanged($event)"></pagination-controls>
</div>
As you can see, we are looping through the all parties and adding a new marker for each party, having checked if the current party has location coordinates available. We are also setting the minimum zoom and zero central coordinates on the map to set whole Earth view point initially.
It turned to be quite easy to add location coordinates to the parties and make changes to the UI, which included Google Maps and location markers on them.
Now we are all set to proceed to more radical visual design changes.