In this chapter we will add Twitter's bootstrap to our project, and add some style and layout to the project.
At the moment, this tutorial we will use only Bootstrap's CSS file and not the JavaScript - but note that you can use all the features of Boostrap 4.
First, we need to add Boostrap 4 to our project - so let's do that.
Run the following command in your Terminal:
$ meteor npm install [email protected] --save
1
2
3
4
5
import angular from 'angular';
import 'bootstrap/dist/css/bootstrap.css';
import { Meteor } from 'meteor/meteor';
And it will import Boostrap's CSS to your project.
OK, simple styling works, but we want to be able to use LESS.
We can't add LESS from NPM because it is a compiler and we want it to be a part of Meteor build - so we will add it from Atmosphere:
$ meteor add less
We will use LESS in a few steps!
Now let's add some style! We will set just a background color.
1
2
3
body {
background-color: #f9f9f9;
}
Let's move loginButton to Navigation and set .container-fluid to the uiView directive.
1
2
3
<navigation class="navbar navbar-static-top navbar-dark bg-inverse"></navigation>
<div ui-view="" class="container-fluid"></div>
Converting to Bootstrap doesn't stop here. By applying bootstrap styles to various other parts of our Socially app, our website will look better on different screens. Have a look at Code Diff to see how we changed the structure of the main files.
Now we can create .less file for Socially:
1
2
3
socially {
display: block;
}
And apply it to main less file:
1
2
3
4
5
body {
background-color: #f9f9f9;
}
@import "../imports/ui/components/socially/socially.less";
To make bootstrap working with all sizes of screens:
1
2
3
4
5
6
7
8
9
10
11
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="/">
<title>Socially</title>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDbphq9crcdpecbseKX3Yx2LPxMRqWK-rc"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
</head>
<body>
<socially></socially>
Move loginButtons under Navigation and set as a bootstrap's navbar:
1
2
3
4
5
6
<div class="fluid-container">
<div class="navbar-header">
<a href="/parties" class="navbar-brand">Socially</a>
<login-buttons class="navbar-brand"></login-buttons>
</div>
</div>
1
2
3
navigation {
display: block;
}
We will use bootstrap's grid system and make all warnings look a lot better:
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
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<party-add ng-show="partiesList.isLoggedIn"></party-add>
<div class="alert alert-warning" role="alert" ng-hide="partiesList.isLoggedIn">
<strong>Warning!</strong>
Log in to create a party!
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h2>List of parties:</h2>
<form class="form-inline">
<div class="form-group">
<input type="search" ng-model="partiesList.searchText" placeholder="Search" class="form-control"/>
</div>
<parties-sort class="form-group" on-change="partiesList.sortChanged(sort)" property="name" order="1"></parties-sort>
</form>
</div>
</div>
<div class="row">
<div class="col-md-6">
<ul class="parties">
<li dir-paginate="party in partiesList.parties | itemsPerPage: partiesList.perPage" total-items="partiesList.partiesCount">
<div class="row">
<div class="col-sm-8">
<h3 class="party-name">
<a ui-sref="partyDetails({ partyId: party._id })">{{party.name}}</a>
</h3>
<p class="party-description">
{{party.description}}
</p>
</div>
<div class="col-sm-4">
<party-remove party="party" ng-show="partiesList.isOwner(party)"></party-remove>
</div>
</div>
<div class="row">
<div class="col-md-12">
<party-rsvp party="party" ng-show="partiesList.isLoggedIn"></party-rsvp>
<div class="alert alert-warning" role="alert" ng-hide="partiesList.isLoggedIn">
<strong>Warning!</strong>
<i>Sign in to RSVP for this party.</i>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<party-creator party="party"></party-creator>
</div>
<div class="col-md-8">
<party-rsvps-list rsvps="party.rsvps"></party-rsvps-list>
</div>
</div>
</li>
</ul>
<dir-pagination-controls on-page-change="partiesList.pageChanged(newPageNumber)"></dir-pagination-controls>
</div>
<div class="col-md-6">
<parties-map parties="partiesList.parties"></parties-map>
</div>
</div>
</div>
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
parties-list {
display: block;
padding: 25px 0;
ul.parties {
padding-left: 0;
list-style-type: none;
> li {
padding: 15px;
background-color: #fff;
margin: 15px 0;
border: 3px solid #eaeaea;
.party-name {
margin-top: 0px;
margin-bottom: 20px;
a {
text-decoration: none !important;
font-weight: 400;
}
}
.party-description {
font-weight: 300;
padding-left: 18px;
font-size: 14px;
}
}
}
}
We will no longer be using PartyUnanswered, time to remove it:
14
15
16
17
18
19
81
82
83
84
85
86
87
import { name as PartyCreator } from '../partyCreator/partyCreator';
import { name as PartyRsvp } from '../partyRsvp/partyRsvp';
import { name as PartyRsvpsList } from '../partyRsvpsList/partyRsvpsList';
class PartiesList {
constructor($scope, $reactive) {
...some lines skipped...
PartyRemove,
PartyCreator,
PartyRsvp,
PartyRsvpsList
]).component(name, {
template,
controllerAs: name,
Let's use .form-group and .form-control classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form>
<div class="form-group">
<label>
Party Name:
</label>
<input type="text" ng-model="partyAdd.party.name" class="form-control"/>
</div>
<div class="form-group">
<label>
Description:
</label>
<input type="text" ng-model="partyAdd.party.description" class="form-control"/>
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="partyAdd.party.public"/> Public Party?
</label>
</div>
<button ng-click="partyAdd.submit()" class="btn btn-success">Add Party!</button>
</form>
1
2
3
4
5
6
7
8
9
10
party-add {
display: block;
}
party-add > form {
padding: 15px;
margin-bottom: 25px;
background-color: #fff;
border: 3px solid #EAEAEC;
}
28
29
30
31
32
}
}
}
@import "../partyAdd/partyAdd.less";
1
2
3
4
5
6
<h4>
See all the parties:
</h4>
<div class="angular-google-map-container">
<ui-gmap-google-map center="partiesMap.map.center" zoom="partiesMap.map.zoom">
<ui-gmap-markers models="partiesMap.parties" coords="'location'" fit="true" idkey="'_id'" doRebuildAll="true"></ui-gmap-markers>
1
2
3
4
5
6
7
8
9
parties-map {
display: block;
margin: 15px 5px;
.angular-google-map-container {
width: 100%;
height: 400px;
}
}
30
31
32
33
}
@import "../partyAdd/partyAdd.less";
@import "../partiesMap/partiesMap.less";
1
2
3
4
<select ng-model="partiesSort.order" ng-change="partiesSort.changed()" class="form-control">
<option value="1">Ascending</option>
<option value="-1">Descending</option>
</select>
Let's add a icon:
1
2
3
4
5
<p>
<small>
<i class="fa fa-user fa-lg"></i> {{ partyCreator.creator | displayNameFilter }}
</small>
</p>
1
2
3
4
5
6
party-creator {
i.fa {
margin-left: 5px;
margin-right: 10px;
}
}
31
32
33
34
@import "../partyAdd/partyAdd.less";
@import "../partiesMap/partiesMap.less";
@import "../partyCreator/partyCreator.less";
We will use icon of X provided by bootstrap v4:
1
<button type="button" class="close" aria-label="Close" ng-click="partyRemove.remove()"><span aria-hidden="true">×</span></button>
Let's make RSVP a lot prettier! User will be able to see how he responded:
1
2
3
<input type="button" value="I'm going!" ng-click="partyRsvp.yes()" class="btn btn-default" ng-class="{'btn-primary' : partyRsvp.isYes()}"/>
<input type="button" value="Maybe" ng-click="partyRsvp.maybe()" class="btn btn-default" ng-class="{'btn-primary' : partyRsvp.isMaybe()}"/>
<input type="button" value="No" ng-click="partyRsvp.no()" class="btn btn-default" ng-class="{'btn-primary' : partyRsvp.isNo()}"/>
1
2
3
4
party-rsvp {
display: block;
margin: 15px 0;
}
32
33
34
35
@import "../partyAdd/partyAdd.less";
@import "../partiesMap/partiesMap.less";
@import "../partyCreator/partyCreator.less";
@import "../partyRsvp/partyRsvp.less";
And create few methods to check the answer:
1
2
3
4
5
6
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
...some lines skipped...
yes() {
this.answer('yes');
}
isYes() {
return this.isAnswer('yes');
}
maybe() {
this.answer('maybe');
}
isMaybe() {
return this.isAnswer('maybe');
}
no() {
this.answer('no');
}
isNo() {
return this.isAnswer('no');
}
answer(answer) {
Meteor.call('rsvp', this.party._id, answer, (error) => {
...some lines skipped...
}
});
}
isAnswer(answer) {
if(this.party) {
return !!_.findWhere(this.party.rsvps, {
user: Meteor.userId(),
rsvp: answer
});
}
}
}
const name = 'partyRsvp';
We will no longer use PartyRsvpUsers, so we can remove it:
2
3
4
5
6
7
9
10
11
12
13
14
15
import angularMeteor from 'angular-meteor';
import template from './partyRsvpsList.html';
class PartyRsvpsList { }
...some lines skipped...
// create a module
export default angular.module(name, [
angularMeteor
]).component(name, {
template,
controllerAs: name,
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
<div class="rsvp-sum">
<div class="rsvp-amount">
<div class="amount">
{{ (partyRsvpsList.rsvps | filter:{rsvp:'yes'}).length || 0 }}
</div>
<div class="rsvp-title">
YES
</div>
</div>
<div class="rsvp-amount">
<div class="amount">
{{ (partyRsvpsList.rsvps | filter:{rsvp:'maybe'}).length || 0 }}
</div>
<div class="rsvp-title">
MAYBE
</div>
</div>
<div class="rsvp-amount">
<div class="amount">
{{ (partyRsvpsList.rsvps | filter:{rsvp:'no'}).length || 0 }}
</div>
<div class="rsvp-title">
NO
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.rsvp-sum {
width: 160px;
@media (min-width: 400px) {
float: right;
}
@media (max-width: 400px) {
margin: 0 auto;
}
> .rsvp-amount {
display: inline-block;
text-align: center;
width: 50px;
> .amount {
font-weight: bold;
font-size: 20px;
}
> .rsvp-title {
font-size: 11px;
color: #aaa;
text-transform: uppercase;
}
}
}
33
34
35
36
@import "../partiesMap/partiesMap.less";
@import "../partyCreator/partyCreator.less";
@import "../partyRsvp/partyRsvp.less";
@import "../partyRsvpsList/partyRsvpsList.less";
1
2
3
4
5
6
7
8
9
10
<h4>Users to invite:</h4>
<ul>
<li ng-repeat="user in partyUninvited.users | uninvitedFilter:partyUninvited.party">
<button ng-click="partyUninvited.invite(user)" class="btn btn-primary-outline">Invite</button>
{{ user | displayNameFilter }}
</li>
</ul>
<div class="alert alert-success" role="alert" ng-if="(partyUninvited.users | uninvitedFilter:partyUninvited.party).length <= 0">
Everyone are already invited.
</div>
1
2
3
4
5
6
7
8
party-uninvited {
display: block;
ul {
padding-left: 0;
list-style-type: none;
}
}
Let's do pretty much the same as we did with PartyAdd and PartyDetails:
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
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<form>
<fieldset class="form-group">
<label>Party name</label>
<input type="text" ng-model="partyDetails.party.name" ng-disabled="!partyDetails.isOwner" class="form-control"/>
</fieldset>
<fieldset class="form-group">
<label>Description</label>
<input type="text" ng-model="partyDetails.party.description" ng-disabled="!partyDetails.isOwner" class="form-control"/>
</fieldset>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="partyDetails.party.public" ng-disabled="!partyDetails.isOwner"/>
Public Party?
</label>
</div>
<button ng-click="partyDetails.save()" type="submit" class="btn btn-primary">Save</button>
</form>
</div>
<div class="col-md-6">
<party-map location="partyDetails.party.location"></party-map>
</div>
</div>
<div class="row">
<div class="col-md-6">
<party-uninvited party="partyDetails.party" ng-show="partyDetails.canInvite()"></party-uninvited>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
party-details {
display: block;
form {
margin: 25px 0;
}
}
@import "../partyUninvited/partyUninvited.less";
We will remove partyMap.css and replace it with partyMap.less:
1
2
3
4
5
6
7
8
9
10
party-map {
display: block;
width: 100%;
margin: 25px 0;
.angular-google-map-container {
width: 100%;
height: 400px;
}
}
3
4
5
6
7
8
import 'angular-simple-logger';
import 'angular-google-maps';
import template from './partyMap.html';
class PartyMap {
7
8
9
10
}
@import "../partyUninvited/partyUninvited.less";
@import "../partyMap/partyMap.less";
Now we can import all less files of direct Socially dependencies:
1
2
3
4
5
6
7
8
socially {
display: block;
}
@import "../navigation/navigation.less";
@import "../partiesList/partiesList.less";
@import "../partyDetails/partyDetails.less";
That's it! Now we have a nice style with a better looking CSS using Bootstrap and LESS!
We learned how to use CSS, LESS and Bootstrap in Meteor.