In this chapter you'll add UI Bootstrap's async typeahead.
An overview and directory for the entire project is in the README.md.
- [Set Up Bootstrap](## Set Up Bootstrap)
- [UI Bootstrap CDN](## UI Bootstrap CDN)
- [App.js Dependency Injection](## App.js Dependency Injection)
- [Typeahead Plugin](## Typeahead Plugin)
- [Controller Dependency Injection](## Controller Dependency Injection)
- [Handlers in Controller](## Handlers in Controller)
- [Display Movies in Index View (Homepage)](## Display Movies in Index View (Homepage))
- [Style Movie Posters](## Style Movie Posters)
- [Add More Cruddy Movies](## Add More Cruddy Movies)
We'll set up the HTML templates (views) now. In home.html
start with the Bootstrap row
container:
<div class="row">
</div>
We need to add some movies.
Let's make a form for adding movies. We could make a form with a dozen inputs for the movie title, poster, actors, director, year, etc. and expect users to type in all this data. Or we can make a typeahead enabling the user to enter one word of a movie title and the typeahead finds the movie in the Open Movies Database (OMDb), then downloads the data.
UI Bootstrap includes a typeahead plugin. UI Bootstrap is the JavaScript plugin library for Angular. Standard Bootstrap JavaScript plugins are incompatible with Angular.
We already added the UI Bootstrap CDN to index.html
. Here's more detailed instructions on this critical step:
The UI Bootstrap CDN must be below the Angular CDN.
You can find the latest CDN. This website gives you a choice of four CDNs:
- Two with templates that start with
ui-bootstrap-tpls
. - Two without templates, that start with
ui-bootstrap
. - Two minified files that are small and load quickly, but can't be read by humans, and end in
min.js
. - Two not minified files that are bigger but humans can read (and change) the code, and end in
js
.
We'll use the CDN with templates, minified:
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/1.2.5/ui-bootstrap-tpls.min.js"></script>
Link only one of the four CDNs. Adding a second CDN will cause errors.
Alternatively you can also download the library from the UI Bootstrap website. There's a big purple button that says
Download
. Move the file into your project folder and make a local link.
Add the dependencies ui.bootstrap
and ui.bootstrap.typeahead
to app.js
:
var app = angular.module("CRUDiestMoviesFirebase", ['ngRoute', 'firebase', `ui.bootstrap`, `ui.bootstrap.typeahead`]);
Go to UI Bootstrap and scroll down to the bottom. The last plugin is Typeahead
. You'll see there are five types of typeahead plugins:
- Four of the plugins --
Static arrays
,ngModelOptions support
,Custom templates for results
, andCustom popup templates for typeahead's dropdown
-- look up in an array of values in your controller. This is ideal when you have a limited number of options, e.g., the fifty states. The four choices format the values differently for the user, including one that adds a state flag downloaded live from Wikipedia. - The
Asynchronous results
plugin goes to any database on the Internet with an API. We'll use this to connect to the Internet Movie Database (IMDB).
Add this code to home.html
.
<!-- Add movie form -->
<form class="form-horizontal">
<input type="text"
class="form-control addMovie"
ng-model="movie.title"
uib-typeahead="address for address in getLocation($viewValue)"
typeahead-loading="loadingLocations"
typeahead-no-results="noResults"
typeahead-min-length="3"
typeahead-on-select="onSelect($item)"
placeholder="Add the worst movie you've seen!"/>
<span class="glyphicon glyphicon-search form-control-feedback"></span>
<i ng-show="loading" class="glyphicon glyphicon-refresh"></i>
<div ng-show="noResults">
<i class="glyphicon glyphicon-remove"></i> No Results Found
</div>
</form>
This form is styled as Bootstrap class form-horizontal
. The input is a text entry field styled as Bootstrap class form-control
. We add a class addMovie
for additional styling.
We connect the form to the $scope
with ng-model
set to the movie title.
The next five lines set the UI Bootstrap typeahead parameters.
The placeholder tells the user what to do.
The search
glyphicon puts a search icon in the box.
The refresh
glyphicon tells the user that data is being downloaded.
Lastly, the remove
glyphicon tells the user that no movie was found.
We introduced a new variable, loading
. When we introduce a new variable we always set the default value in the controller. In HomeController.js
add this near the top:
// Set variables
$scope.loading = false;
In HomeController.js
inject the dependency $http
:
app.controller('HomeController', ['$scope', `$http`, '$firebaseArray', function($scope, $http, $firebaseArray) {
console.log("Home controller.");
}]);
The handler $scope.getLocation
uses the $http
service to send an HTTP request to the Open Movies Database (OMDb).
In HomeController.js
we'll add two handlers for the typeahead:
$scope.getLocation = function(val) {
return $http.get('//www.omdbapi.com/?s=' + val) // send an HTTP request to the OMDb
.then(function(response){ // then execute a promise
return response.data.Search.map(function(item){ // when OMDb can't find a movie to match the search string an error is logged "TypeError: Cannot read property 'map' of undefined". This error can be ignored, when the OMDb finds a movie then no error is logged.
return item.Title;
});
}, function(error) {
console.log(error);
});
};
$scope.onSelect = function ($item) {
$scope.loading = true; // switch on the "downloading data" glyphicon
$scope.movie.title = null; // needed to prevent previous query from autofilling search form
console.log("Selected!");
return $http.get('//www.omdbapi.com/?t=' + $item) // send an HTTP request to the OMDb to get a movie object
.then(function(response){ // then execute a promise
var movie = { // make a movie object locally matching the downloaded OMDb movie object
actors: response.data.Actors, // local fields are filled with data from the OMDb
awards: response.data.Awards,
comments: [],
country: response.data.Country,
director: response.data.Director,
genre: response.data.Genre,
language: response.data.Language,
likes: 0,
metascore: response.data.Metascore,
plot: response.data.Plot,
poster: response.data.Poster,
rated: response.data.Rated,
runtime: response.data.Runtime,
title: response.data.Title,
writer: response.data.Writer,
year: response.data.Year,
imdbID: response.data.imdbID,
imdbRating: response.data.imdbRating,
imdbVotes: response.data.imdbVotes,
dateAdded: Date.now()
};
$scope.movies.$add(movie).then(function() { // use a Firebase array method to add the new movie object to our movies array
$scope.order = '$id' // reset orderBy so that new movie appears in upper left
$scope.loading = false; // switch off the "downloading data" glyphicon
});
});
};
The $scope.getLocation()
handler queries the OMDb for the movie and returns a drop-down menu of ten movies for the user to choose from. Note: you'll see an error message logged in the console "TypeError: Cannot read property 'map' of undefined" for every keystroke in which OMDb can't find a matching movie title word. This error message can be ignored.
When the user selects a movie from the list $scope.onSelect()
fires and adds the movie object to our array of movies. The $scope.loading
variable switches on to show the refresh
glyphicon. The next line prevents the previous query from autofilling the search form.
We create a movie object using data from the OMDb, plus a few fields of our own:
comments
is an empty array, ready for users to add comments.likes
is initialized at zero, ready for users to like or dislike a movie.dateAdded
takes the current time and date.
We run the AngularFire method $add() to add the movie object to our movies array. This is similar to the JavaScript method push()
.
After the movie object is added to the movies array a promise is executed. We reset the viewing order so that the new movie appears in the upper left position. Then we switch the $scope.loading
variable off to hide the refresh
glyphicon.
Deploy to Firebase and see if it works.
Select a movie, then go to your Firebase Dashboard and the movie should be there.
Now we'll display our movies in home.html
:
<div ng-repeat="movie in movies | orderBy : order : reverse" class="movieIndex">
<a ng-href="/#/movies/{{movie.$id}}"><img class="largeposter" ng-src="{{movie.poster}}" alt="{{movie.title}}"></a>
</div>
This will display all the movie objects in our movies array, ordered by reverse date added. The img
displays the movie poster, and when the user clicks on the poster the route changes to the SHOW
page.
We introduced the variable reverse
. Set its default value in HomeController.js
:
// Set variables
$scope.loading = false;
$scope.reverse = true;
The Angular filter | orderBy : order : reverse
sets the order of the movies. order
is the variable $scope.order
. reverse
sets reverse order.
We need to set the default for the variable $scope.order
. The default should be dataAdded
so that when a user adds a new movie it appears in the upper left position, providing UI/UX feedback for the user's action.
We introduced the variable order
. Set its default value in HomeController.js
:
// Set variables
$scope.loading = false;
$scope.reverse = true;
$scope.order = 'dateAdded';
The movies posters display in a column down the left side of the browser window. Let's add CSS styling to make the movies display in rows. In style.css
:
.movieIndex {
display: inline;
}
Deploy to Firebase, refresh your browser, and you should see your first movie.
Add at least six more cruddy movies. Here are some lists of cruddy movies to add to your database:
- Wikipedia's Worst Movies List
- IMDb's Bottom 100
- Rotten Tomatoes' 25 Movies So Bad They're Unmissable
- CheatSheet's 10 Worst Movies of All Time
Files changed in this chapter:
app.js
home.html
HomeController.js
style.css
Your app.js
should look like this:
var app = angular.module("CRUDiestMoviesFirebase", ['ngRoute', 'firebase', `ui.bootstrap`, `ui.bootstrap.typeahead`]);
Your home.html
should now look like this:
<div class="row">
<!-- Add movie form -->
<form class="form-horizontal">
<input type="text"
class="form-control addMovie"
ng-model="movie.title"
uib-typeahead="address for address in getLocation($viewValue)"
typeahead-loading="loadingLocations"
typeahead-no-results="noResults"
typeahead-min-length="3"
typeahead-on-select="onSelect($item)"
placeholder="Add the worst movie you've seen!"/>
<span class="glyphicon glyphicon-search form-control-feedback"></span>
<i ng-show="loading" class="glyphicon glyphicon-refresh"></i>
<div ng-show="noResults">
<i class="glyphicon glyphicon-remove"></i> No Results Found
</div>
</form>
<div ng-repeat="movie in movies | orderBy : order : reverse" class="movieIndex">
<a ng-href="/#/movies/{{movie.$id}}"><img class="largeposter" ng-src="{{movie.poster}}" alt="{{movie.title}}"></a>
</div>
</div>
Your HomeController.js
should now look like this:
app.controller('HomeController', ['$scope', `$http`, '$firebaseArray', function($scope, $http, $firebaseArray) {
console.log("Home controller.");
var ref = new Firebase("https://crudiest-movies-fire.firebaseio.com/");
$scope.movies = $firebaseArray(ref);
// Set variables
$scope.loading = false;
$scope.reverse = true;
$scope.order = 'dateAdded';
$scope.getLocation = function(val) {
return $http.get('//www.omdbapi.com/?s=' + val) // send an HTTP request to the OMDb
.then(function(response){ // then execute a promise
return response.data.Search.map(function(item){ // when OMDb can't find a movie to match the search string an error is logged "TypeError: Cannot read property 'map' of undefined". This error can be ignored, when the OMDb finds a movie then no error is logged.
return item.Title;
});
}, function(error) {
console.log(error);
});
};
$scope.onSelect = function ($item) {
$scope.loading = true; // switch on the glyphicon to indicate that the data is loading
$scope.movie.title = null; // needed to prevent previous query from autofilling search form
console.log("Selected!");
return $http.get('//www.omdbapi.com/?t=' + $item) // send an HTTP request to the OMDb to get a movie object
.then(function(response){ // then execute a promise
var movie = { // make a movie object locally matching the downloaded OMDb movie object
actors: response.data.Actors, // local fields are filled with data from the OMDb
awards: response.data.Awards,
comments: [],
country: response.data.Country,
director: response.data.Director,
genre: response.data.Genre,
language: response.data.Language,
likes: 0,
metascore: response.data.Metascore,
plot: response.data.Plot,
poster: response.data.Poster,
rated: response.data.Rated,
runtime: response.data.Runtime,
title: response.data.Title,
writer: response.data.Writer,
year: response.data.Year,
imdbID: response.data.imdbID,
imdbRating: response.data.imdbRating,
imdbVotes: response.data.imdbVotes,
dateAdded: Date.now()
};
$scope.movies.$add(movie).then(function() { // use a Firebase array method to add the new movie object to our movies array
$scope.order = '$id' // reset orderBy so that new movie appears in upper left
$scope.loading = false; // switch off the "downloading data" glyphicon
});
});
};
}]);
Your style.css
should now look like this:
.movieIndex {
display: inline;
}
Deploy to Firebase:
firebase deploy
and save to GitHuB:
git status
git add .
git commit -m "Finished async typeahead."
git push origin master