Fork me on GitHub
Note: The Socially 2 tutorial is no longer maintained. Instead, we have a new, better and comprehensive tutorial, integrated with our WhatsApp clone tutorial: WhatsApp clone tutorial with Ionic 2, Angular 2 and Meteor (we just moved all features and steps, and implemented them better!)

CSS, SASS and Bootstrap

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

In this chapter we will add Twitter's bootstrap to our project, and add some style and layout to the project.

Adding and importing Bootstrap 4

First, we need to add Bootstrap 4 to our project - so let's do that.

Run the following command in your Terminal:

$ meteor npm install --save [email protected]

Import Bootstrap's styles into your project:

18.2 Import bootstrap style into the main style file client/main.scss
1
2
3
4
5
@import "{}/node_modules/bootstrap/scss/bootstrap.scss";
 
.sebm-google-map-container {
  width: 400px;
  height: 400px;

First touch of style

Now let's add some style! we will add navigation bar in the top of the page.

We will also add a container with the router-outlet to keep that content of the page:

18.3 Add bootstrap navbar client/imports/app/app.component.html
1
2
3
4
5
6
<nav class="navbar navbar-light bg-faded">
  <a class="navbar-brand" href="#">Socially</a>
</nav>
<div class="container-fluid">
  <router-outlet></router-outlet>
</div>

Moving things around

So first thing we want to do now, is to move the login buttons to another place - let's say that we want it as a part of the navigation bar.

So first let's remove it from it's current place (parties list), first the view:

18.4 Remove LoginButtons from a template client/imports/app/parties/parties-list.component.html
2
3
4
5
6
7
  <parties-form [hidden]="!user" style="float: left"></parties-form>
  <input type="text" #searchtext placeholder="Search by Location">
  <button type="button" (click)="search(searchtext.value)">Search</button>
 
  <h1>Parties:</h1>
 

And add it to the main component, which is the component that responsible to the navigation bar, so the view first:

18.5 Add login buttons to the navigation bar client/imports/app/app.component.html
1
2
3
4
5
6
<nav class="navbar navbar-light bg-faded">
  <a class="navbar-brand" href="#">Socially</a>
  <login-buttons class="pull-right"></login-buttons>
</nav>
<div class="container-fluid">
  <router-outlet></router-outlet>

Fonts and FontAwesome

Meteor gives you the control of your head tag, so you can import fonts and add your meta tags.

We will add a cool font and add FontAwesome style file, which also contains it's font:

18.6 Add fonts and FontAwesome client/index.html
1
2
3
4
5
6
7
8
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <base href="/">
  <link href='http://fonts.googleapis.com/css?family=Muli:400,300' rel='stylesheet' type='text/css'>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
</head>
<body>
  <app>Loading...</app>

Some more style

So now we will take advantage of all Bootstrap's features - first let's update the layout of the form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<form [formGroup]="addForm" (ngSubmit)="addParty()" class="form-inline">
  <fieldset class="form-group">
    <label for="partyName">Party name</label>
    <input id="partyName" class="form-control" type="text" formControlName="name" placeholder="Party name" />
 
    <label for="description">Description</label>
    <input id="description" class="form-control" type="text" formControlName="description" placeholder="Description">
 
    <label for="location_name">Location</label>
    <input id="location_name" class="form-control" type="text" formControlName="location" placeholder="Location name">
 
    <div class="checkbox">
      <label>
        <input type="checkbox" formControlName="public">
        Public
      </label>
    </div>
 
    <button type="submit" class="btn btn-primary">Add</button>
  </fieldset>
</form>

And now the parties list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<div class="row">
  <div class="col-md-12">
    <div class="jumbotron">
      <h3>Create a new party!</h3>
      <parties-form [hidden]="!user"></parties-form>
      <div [hidden]="user">You need to login to create new parties!</div>
    </div>
  </div>
</div>
<div class="row ma-filters">
  <div class="col-md-6">
    <h3>All Parties:</h3>
    <form class="form-inline">
      <input type="text" class="form-control" #searchtext placeholder="Search by Location">
      <button type="button" class="btn btn-primary" (click)="search(searchtext.value)">Search</button>
      Sort by name: <select class="form-control" #sort (change)="changeSortOrder(sort.value)">
      <option value="1" selected>Ascending</option>
      <option value="-1">Descending</option>
    </select>
    </form>
  </div>
</div>
<div class="row">
  <div class="col-md-6">
    <ul class="list-group">
      <li class="list-group-item">
        <pagination-controls (pageChange)="onPageChanged($event)"></pagination-controls>
      </li>
      <li *ngFor="let party of parties | async"
          class="list-group-item ma-party-item">
        <div class="row">
          <div class="col-sm-8">
            <h2 class="ma-party-name">
              <a [routerLink]="['/party', party._id]">{{party.name}}</a>
            </h2>
            @ {{party.location.name}}
            <p class="ma-party-description">
              {{party.description}}
            </p>
          </div>
          <div class="col-sm-4">
            <button class="btn btn-danger pull-right" [hidden]="!isOwner(party)" (click)="removeParty(party)"><i
              class="fa fa-times"></i></button>
          </div>
        </div>
        <div class="row ma-party-item-bottom">
          <div class="col-sm-4">
            <div class="ma-rsvp-sum">
              <div class="ma-rsvp-amount">
                <div class="ma-amount">
                  {{party | rsvp:'yes'}}
                </div>
                <div class="ma-rsvp-title">
                  YES
                </div>
              </div>
              <div class="ma-rsvp-amount">
                <div class="ma-amount">
                  {{party | rsvp:'maybe'}}
                </div>
                <div class="ma-rsvp-title">
                  MAYBE
                </div>
              </div>
              <div class="ma-rsvp-amount">
                <div class="ma-amount">
                  {{party | rsvp:'no'}}
                </div>
                <div class="ma-rsvp-title">
                  NO
                </div>
              </div>
            </div>
          </div>
        </div>
      </li>
      <li class="list-group-item">
        <pagination-controls (pageChange)="onPageChanged($event)"></pagination-controls>
      </li>
    </ul>
  </div>
  <div class="col-md-6">
    <ul class="list-group">
      <li class="list-group-item">
        <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>
      </li>
    </ul>
  </div>
</div>

Styling components

We will create style file for each component.

So let's start with the parties list, and add some style (it's not that critical at the moment what is the effect of those CSS rules)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@import "../colors";
 
.ma-add-button-container {
  button.btn {
    background: $color3;
    float: right;
    margin-right: 5px;
    outline: none;
    i {
      color: $color5;
    }
  }
}
 
.ma-parties-col {
  padding-top: 20px;
}
 
.ma-filters {
  margin-bottom: 10px;
}
 
.ma-party-item {
  .ma-party-name {
    margin-bottom: 20px;
    a {
      color: $color6;
      text-decoration: none !important;
      font-weight: 400;
    }
  }
  .ma-party-description {
    color: $color6;
    font-weight: 300;
    padding-left: 18px;
    font-size: 14px;
  }
 
  .ma-remove {
    color: lighten($color7, 20%);
    position: absolute;
    right: 20px;
    top: 20px;
    &:hover {
      color: $color7;
    }
  }
 
  .ma-party-item-bottom {
    padding-top: 10px;
    .ma-posted-by-col {
      .ma-posted-by {
        color: darken($color4, 30%);
        font-size: 12px;
      }
      .ma-everyone-invited {
        @media (max-width: 400px) {
          display: block;
          i {
            margin-left: 0px !important;
          }
        }
        font-size: 12px;
        color: darken($color4, 10%);
        i {
          color: darken($color4, 10%);
          margin-left: 5px;
        }
      }
    }
 
    .ma-rsvp-buttons {
      input.btn {
        color: darken($color3, 20%);
        background: transparent !important;
        outline: none;
        padding-left: 0;
        &:active {
          box-shadow: none;
        }
        &:hover {
          color: darken($color3, 30%);
        }
        &.btn-primary {
          color: lighten($color3, 10%);
          border: 0;
          background: transparent !important;
        }
      }
    }
 
    .ma-rsvp-sum {
      width: 160px;
      @media (min-width: 400px) {
        float: right;
      }
      @media (max-width: 400px) {
        margin: 0 auto;
      }
    }
    .ma-rsvp-amount {
      display: inline-block;
      text-align: center;
      width: 50px;
      .ma-amount {
        font-weight: bold;
        font-size: 20px;
      }
      .ma-rsvp-title {
        font-size: 11px;
        color: #aaa;
        text-transform: uppercase;
      }
    }
  }
}
 
.ma-angular-map-col {
  .angular-google-map-container, .angular-google-map {
    height: 100%;
    width: 100%;
  }
}
 
.search-form {
  margin-bottom: 1em;
}

Note that we used the "colors.scss" import - don't worry - we will add it soon!

And now let's add SASS file for the party details:

18.10 Add styles for the party details page client/imports/app/parties/party-details.component.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.ma-party-details-container {
  padding: 20px;
 
  .angular-google-map-container {
    width: 100%;
    height: 100%;
  }
 
  .angular-google-map {
    width: 100%;
    height: 400px;
  }
 
  .ma-map-title {
    font-size: 16px;
    font-weight: bolder;
  }
 
  .ma-invite-list {
    margin-top: 20px;
    margin-bottom: 20px;
 
    h3 {
      font-size: 16px;
      font-weight: bolder;
    }
 
    ul {
      padding: 0;
    }
  }
}

Now let's add some styles and colors using SASS to the main file and create the colors definitions file we mentioned earlier:

18.11 Add components styles in main style entry point client/imports/app/colors.scss
1
2
3
4
5
6
7
$color1 : #2F2933;
$color2 : #01A2A6;
$color3 : #29D9C2;
$color4 : #BDF271;
$color5 : #FFFFA6;
$color6 : #2F2933;
$color7 : #FF6F69;
18.11 Add components styles in main style entry point client/main.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@import "../node_modules/bootstrap/scss/bootstrap.scss";
@import "./imports/app/colors.scss";
 
html, body {
  height: 100%;
}
 
body {
  background-color: #f8f8f8;
  font-family: 'Muli', sans-serif;
}
 
.sebm-google-map-container {
  width: 450px;
  height: 450px;
}
 
.navbar {
  background-color: #ffffff;
  border-bottom: #eee 1px solid;
  color: $color3;
  font-family: 'Muli', sans-serif;
  a {
    color: $color3;
    text-decoration: none !important;
  }
 
  .navbar-right-container {
    position: absolute;
    top: 17px;
    right: 17px;
  }
}

We defined our colors in this file, and we used it all across the our application - so it's easy to change and modify the whole theme!

Now let's use Angular 2 Component styles, which bundles the styles into the Component, without effecting other Component's styles (you can red more about it here)

So let's add it to the parties list:

13
14
15
16
17
18
19
 
26
27
28
29
30
31
32
33
import { Party } from '../../../../both/models/party.model';
 
import template from './parties-list.component.html';
import style from './parties-list.component.scss';
 
interface Pagination {
  limit: number;
...some lines skipped...
 
@Component({
  selector: 'parties-list',
  template,
  styles: [ style ]
})
@InjectUser('user')
export class PartiesListComponent implements OnInit, OnDestroy {

And to the party details:

15
16
17
18
19
20
21
22
23
24
25
26
import { User } from '../../../../both/models/user.model';
 
import template from './party-details.component.html';
import style from './party-details.component.scss';
 
@Component({
  selector: 'party-details',
  template,
  styles: [ style ]
})
@InjectUser('user')
export class PartyDetailsComponent implements OnInit, OnDestroy {

And use those new cool styles in the view of the party details:

18.14 Update the layout of the party details page client/imports/app/parties/party-details.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<div class="row ma-party-details-container">
  <div class="col-sm-6 col-sm-offset-3">
    <legend>View and Edit Your Party Details:</legend>
    <form class="form-horizontal" *ngIf="party" (submit)="saveParty()">
      <div class="form-group">
        <label>Party Name</label>
        <input [disabled]="!isOwner" type="text" [(ngModel)]="party.name" name="name" class="form-control">
      </div>
 
      <div class="form-group">
        <label>Description</label>
        <input [disabled]="!isOwner" type="text" [(ngModel)]="party.description" name="description" class="form-control">
      </div>
 
      <div class="form-group">
        <label>Location name</label>
        <input [disabled]="!isOwner" type="text" [(ngModel)]="party.location.name" name="location" class="form-control">
      </div>
 
      <div class="form-group">
        <button [disabled]="!isOwner" type="submit" class="btn btn-primary">Save</button>
        <a [routerLink]="['/']" class="btn">Back</a>
      </div>
    </form>
 
    <ul class="ma-invite-list" *ngIf="isOwner || isPublic">
      <h3>
        Users to invite:
      </h3>
      <li *ngFor="let user of users | async">
        <div>{{ user | displayName }}</div>
        <button (click)="invite(user)" class="btn btn-primary btn-sm">Invite</button>
      </li>
    </ul>
 
    <div *ngIf="isInvited">
      <h2>Reply to the invitation</h2>
      <input type="button" class="btn btn-primary" value="I'm going!" (click)="reply('yes')">
      <input type="button" class="btn btn-warning" value="Maybe" (click)="reply('maybe')">
      <input type="button" class="btn btn-danger" value="No" (click)="reply('no')">
    </div>
 
    <h3 class="ma-map-title">
      Click the map to set the party location
    </h3>
 
    <div class="angular-google-map-container">
      <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>
    </div>
  </div>
</div>

Summary

So in this chapter of the tutorial we added the Bootstrap library and used it's layout and CSS styles.

We also learned how to integrate SASS compiler with Meteor and how to create isolated SASS styles for each component.