Google Maps

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

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.

Adding Location Coordinates

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:

17.1 Extend party location with coords both/models/party.model.ts
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:

17.2 Change initial parties accordingly server/imports/fixtures/parties.ts
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:

17.3 Change party creation component client/imports/app/parties/parties-form.component.ts
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:

17.4 Reflect type changes in the parties search server/imports/publications/parties.ts
60
61
62
63
64
65
66
 
  return {
    $and: [{
        'location.name': searchRegEx
      },
      isAvailable
    ]

And also let's update it in the view:

17.7 Change ngModel to location.name client/imports/app/parties/party-details.component.html
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.

Adding Google Maps

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:

17.6 Import google maps module client/imports/app/app.module.ts
4
5
6
7
8
9
10
 
18
19
20
21
22
23
24
25
26
27
import { RouterModule } from [email protected]/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:

17.9 Add maps logic to the component client/imports/app/parties/party-details.component.ts
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:

  • when the user visit a newly created party, she will see a map centered at Palo Alto;
  • if she clicks on some part of the map, a new marker will be placed at that place;
  • if she decides to save the party changes, new location coordinates will be saved as well;
  • on the next visit, the map will be centered at the saved party location with a marker shown at this point.

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:

17.10 Add maps styles client/main.scss
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.

Multiple Markers

Adding multiple markers on the parties front page should be straightforward now. Here is the markup:

17.11 Add all parties locations on the map client/imports/app/parties/parties-list.component.html
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.

Summary

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.