This is the second part of Building SPA using AngularJS Series. The topics we’ll cover are:
- Building SPA using AngularJS – Part 1
- Building SPA using AngularJS – Part 2 (This Post)
- Building SPA using AngularJS – Part 3
Update (2014-May-5) New post which covers adding GulpJS as Task Runner for our AngularJS application:
Update (2014-June-1) New post which covers Implementing AngularJS Token Authentication using ASP.NET Web API 2 and Owin middleware:
Building SPA using AngularJS – Part 2
In the previous post we have done all work needed to bootstrap our app with Angular, now we’ll focus on implementing the use cases we mentioned in the introductory post, here we’ll see how powerful and fun working with Angular can be. I will start building those requirements from bottom to top, so we’ll start by services then we move up to the view.
Step 1: Adding service to communicate with Foursquare API:
As we discussed before, services in Angular are singleton objects responsible of doing certain tasks, or they can be used for sharing business logic. To use those custom created services we just need to specify service name in controller, filters, directive, etc… and Angular will inject those singleton objects. Angular is built in a way that it will be able to manage those custom created services so we can use them any place in our app.
There are different ways to create Angular Services, two of them are Service or Factory methods, both will provide us with a singleton shared object, and the only difference is how they are created, I won’t dig more into differences and we’ll use Factory for creating Services. You can read this informative stackoverflow question which highlights the differences.
So let’s add new JS file named “placesExplorerService.js” to the folder “app–>services”, this service will be responsible to send HTTP requests to Foursquare explore places API. For more information about Foursquare explore places API and how you can get your own clientID and secret you can check the official documentation.
Now open the file and paste the code snippet below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var requestParms = { clientId: "DO5JJHGXBODWHZUZ2W45T0S35PKJH3MCLC1SKF5U4X3VF4YA", clientSecret: "GF0PDCNGEKSU2GI4ANGBGBKTEUU0G3E3QYPO5YWFXRV33GY5", version: "20131230" } app.factory('placesExplorerService', function ($resource) { var requestUri = 'https://api.foursquare.com/v2/venues/:action'; return $resource(requestUri, { action: 'explore', client_id: requestParms.clientId, client_secret: requestParms.clientSecret, v: requestParms.version, venuePhotos: '1', callback: 'JSON_CALLBACK' }, { get: { method: 'JSONP' } }); }); |
By looking at the code above you will notice that creating a factory is simple, all you need to do is calling app.factory() function where app is shared variable declared in app.js file holding our module “FoursquareApp”.
As well you will notice that we are injecting built-in Angular service named “$resource” into our service, this service won’t work until we inject “ngResource” module to our “FoursquareApp” module, this already done into file app.js.
Basically the built-in $resource is a service which allow us to interact with RESTful data sources, we can use another lower level built-in service called $http but in our case $resource service is sufficient.
Now we need to configure the $resource service by passing the params needed by explore places API, you can check the needed mandatory params by visiting their documentation, what is worth mentioning here that :action param will be translated to a part of the request URL and any another params will be treated as query strings (key, value) pairs. For sure we need extra params to pass it for the API, such as the nearby city and category to explore for. Those params will be sent from the controller, we’ll see this soon. Note: you can use those ClientID and Secret to follow up with this demo app, but I recommend to have your own CleintID and secret.
Do not forget to reference the newly created JS file at the bottom of our shell page (index.html) body.
Step 2: Implementing business logic in controller:
Now we’ll start implementing the functions and model needed to interact with our view “placesresults.html”, if you are following from the previous post, we’ve already created a controller named “placesExplorerController” with a single model (exploreNearby), now we need to expand the models and functions needed to build the app, I’ll paste most of the controller code below and will explain what is going in the next paragraph:
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 |
'use strict'; app.controller('placesExplorerController', function ($scope, placesExplorerService, $filter) { $scope.exploreNearby = "New York"; $scope.exploreQuery = ""; $scope.filterValue = ""; $scope.places = []; $scope.filteredPlaces = []; $scope.filteredPlacesCount = 0; //paging $scope.totalRecordsCount = 0; $scope.pageSize = 10; $scope.currentPage = 1; init(); function init() { createWatche(); getPlaces(); } function getPlaces() { var offset = ($scope.pageSize) * ($scope.currentPage - 1); placesExplorerService.get({ near: $scope.exploreNearby, query: $scope.exploreQuery, limit: $scope.pageSize, offset: offset }, function (placesResult) { if (placesResult.response.groups) { $scope.places = placesResult.response.groups[0].items; $scope.totalRecordsCount = placesResult.response.totalResults; filterPlaces(''); } else { $scope.places = []; $scope.totalRecordsCount = 0; } }); }; function filterPlaces(filterInput) { $scope.filteredPlaces = $filter("placeNameCategoryFilter")($scope.places, filterInput); $scope.filteredPlacesCount = $scope.filteredPlaces.length; } function createWatche() { $scope.$watch("filterValue", function (filterInput) { filterPlaces(filterInput); }); } $scope.doSearch = function () { $scope.currentPage = 1; getPlaces(); }; $scope.pageChanged = function (page) { $scope.currentPage = page; getPlaces(); }; $scope.buildCategoryIcon = function (icon) { return icon.prefix + '44' + icon.suffix; }; $scope.buildVenueThumbnail = function (photo) { return photo.items[0].prefix + '128x128' + photo.items[0].suffix; }; }); |
- We’ve injected the “placesExplorerService” and another built-in service named “$filter” into the controller.
- We’ve defined different model objects (exploreNearby, exploreQuery, places array, etc…) where they will be used for two-way data binding between our view “placesresults.html” and the model.
- After we injected the service “placesExplorerService”, and as this code snippet:
1placesExplorerService.get({ near: $scope.exploreNearby, query: $scope.exploreQuery, limit: $scope.pageSize, offset: offset }, function (placesResult) {});
we were able to issue HTTP GET request to the factory we created, note how we can pass any number of arguments to the get method, and all those arguments (i.e. near, query, limit, etc..) will be translated to query string (key, value) pairs, by having this we were able have a complete GET request with the needed params for server side pagination. - We’ve used custom filter which allow us to filter for results returned, we’ll cover how to create filters soon.
- We have created a watch for filterValue, in simple words we’ve added listener for on the “filterValue” attribute of the scope, this listener gets fired when the value of this attribute has changed.
- We’ve added different functions needed to be called from the view, such as “doSearch()” which will be called on the explore button click, as well “pageChanged()” which will be called once the page at the pagination control change.
- Multiple helper functions have been added, those functions are used to build some thumbnails image source.
Now we need to add the filter which will allow users to filter results returned, we’ll be filtering by Place Name or Place Category.
Step 3: Adding custom filter:
To define a custom filter we need to add new JS file named “placeNameCategoryFilter.js” to the folder “app–>filters”, open the file and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
app.filter("placeNameCategoryFilter", function () { return function (places, filterValue) { if (!filterValue) return places; var matches = []; filterValue = filterValue.toLowerCase(); for (var i = 0; i < places.length; i++) { var place = places[i]; if (place.venue.name.toLowerCase().indexOf(filterValue) > -1 || place.venue.categories[0].shortName.toLowerCase().indexOf(filterValue) > -1) { matches.push(place); } } return matches; }; }); |
Defining a filter is straight forward and it is like defining a factory, all we need to do is to call app.filter() function where app is shared variable declared in app.js file holding our module “FoursquareApp”. We can inject services in filters but in our case the filter is simple, it will accept an array of the places returned from Foursquare API, and filter value, then it will filter the places array for the name or category only, the result of this process is a new filtered places array.
To use this custom filter in “placesExplorerController” is simple, we need just to inject the built-in “$filter” service, then we are able to execute our custom filter by calling:
1 |
$filter("placeNameCategoryFilter")($scope.places, filterInput); |
Step 4: Modifying Places Results View:
Till this point we have the service and controller ready, but we need to work on the view in order to project the results in nice way and enable pushing user inputs from the view back to the service, so let’s open the view “placesresults.html” and paste the code snippet below. Note I’m removing the Bootstrap classes to keep it simple, you can view the full html fragments at github.
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 |
<div> <form class="form-horizontal"> <div class="form-group"> <div> <input type="text" data-ng-model="exploreNearby" class="form-control" placeholder="Explore Places In. e.g. New York" /> </div> <div> <input type="text" data-ng-model="exploreQuery" class="form-control" placeholder="Specify Category. e.g. Burger" /> </div> <div> <button class="btn btn-primary pull-right" data-ng-click="doSearch()"><span class="glyphicon glyphicon-search"></span>Explore</button> </div> </div> <div class="well well-sm" data-ng-show="totalRecordsCount == 0"> <h5>No Places found Near by ({{exploreNearby}})</h5> </div> <div data-ng-show="totalRecordsCount > 0"> <div> <div> <input type="text" data-ng-model="filterValue" class="form-control" placeholder="Filter Places by (Place Name) or (Category)" /> </div> </div> <ul class="list-group" data-ng-repeat="item in filteredPlaces"> <li class="list-group-item"> <div class="row"> <div> <img data-ng-src="{{buildCategoryIcon(item.venue.categories[0].icon)}}" title="{{item.venue.categories[0].shortName}}" class="venueIcon" /> </div> <div> <a href="" data-ng-click="showVenuePhotos(item.venue.id,item.venue.name)"> <h2 class="venueName">{{item.venue.name}}</h2> </a> </div> <div> </div> </div> <div class="row"> <div> <span class="badge">{{item.venue.rating | number:1}}</span> </div> <div> <p class="text-warning"><small>{{(item.venue.location.address != null) && item.venue.location.address}} - {{item.venue.categories[0].shortName}}</small></p> <p>{{item.tips[0].text}}</p> <hr class="seperator" /> <a href="" data-ng-click="bookmarkPlace(item.venue)"><span class="glyphicon glyphicon-bookmark"></span>Bookmark Place</a> </div> <div> <a href="" data-ng-click="showVenuePhotos(item.venue.id,item.venue.name)"> <img data-ng-src="{{buildVenueThumbnail(item.venue.photos.groups[0])}}" class="img-thumbnail" /></a> </div> </div> </li> </ul> </div> <div class="row"> <div class="ext-center"> <div data-ng-show="totalRecordsCount > 0"> <div> <div data-pagination="" data-previous-text="<" data-next-text=">" data-first-text="<<" data-last-text=">>" data-on-select-page="pageChanged(page)" data-total-items="totalRecordsCount" data-page="currentPage" data-boundary-links="true" class="pagination pagination-sm" data-max-size="5" data-rotate="false" data-items-per-page="10"> </div> </div> </div> </div> </div> </form> </div> |
Below is an explanation of what we did on this view:
- We’ve used ng-model with model “exploreNearby”, and “exploreQuery”, so once we change the text into the input fields, the models will get updated directly.
- We’ve wired the click event for the explore button with the function “doSearch()” in the controller by using ng-click directives.
- We’ve used ng-repeat to iterate through the array of filtered places then we can bind the view with each place, notice how we created <li> tag for each place.
- Inside ng-repeat we’re able to drill into single place data and print it on the view, for example to print place name we’ve used {{item.venue.name}}, as well we’re able to execute helper functions in this simple way: ng-src=”{{buildCategoryIcon(item.venue.categories[0].icon)}}
- We’ve used built-in filters for formatting decimal numbers {{item.venue.rating | number:1}}, you can find more infor about Angular built-in filters here.
- We’ve implemented pagination using Angular Bootstrap UI directive named “data-pagination“, by using this we’re able to implement server side pagination. This pagination control is flexible, you can check how to customize it here.
- We’ve added a link with title “Bookmark Place” which will be used for saving user favorite places, we’ll implement the function “bookmarkPlace()” in the next post.
What is left here is displaying the top 9 thumbnails for the place once the user click on place name, one good way to implement this is to use $modal service which allow us to open modal dialog on top of our search result page, in the next step we’ll add this modal.
Step 5: Add modal view to view places images
The modal view is normal html view, so let’s add new file named “placesphotos.html” under “app–>views”, I won’t list view html here because its simple and you can check it on github.
To get the images, we need to make HTTP Get request to Foursquare API, this call is some how identical to the previous one, all we need to do here is to create new service named “placesPhotosService” which will be responsible to issue this Get request. So create new JS file named “placesPhotosService.js” under “app–>services”, you can check it’s code on github.
As we mentioned earlier each view has it’s unique controller, so we need to create new controller named “placesPhotosController”, so add new JS file named “placesPhotosController.js” under “app–>controllers”. You can check controller code on github.
Till this moment Angular is not aware that the new view and controller created are tied together, previously we did this using $routeProvider while booting up our application, but in the case of using $modal service we can inform Angular about this relation once we insentiate the modal, remember that we want to display the view as a modal not as partial view, take a look on the code snippet below inside “placesExplorerController”:
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 |
$scope.showVenuePhotos = function (venueId, venueName) { placesPhotosService.get({ venueId: venueId }, function (photosResult) { var modalInstance = $modal.open({ templateUrl: 'app/views/placesphotos.html', controller: 'placesPhotosController', resolve: { venueName: function () { return venueName; }, venuePhotos: function () { return photosResult.response.photos.items; } } }); modalInstance.result.then(function () { //$scope.selected = selectedItem; }, function () { //alert('Modal dismissed at: ' + new Date()); }); }); }; |
By looking at the code above, you will notice that we’ve injected the new service “placesPhotosService” into the controller so we can issue HTTP Get request, as well we’ve injected the service $modal which will be responsible to open the modal dialog. Once we receive the 9 thumbnails successfully from the API, we’ll call $modal.open and inject the template URL, and controller in the configuration section, once this is done Angular will be aware that this template is mapped to this controller. As well we’ve passed the place name and array of 9 thumbnails to the “placesPhotosController”. The final result will be as the image below:
In the next post we’ll add the last functionality in our application which will allow end users to bookmark their favorite places.
In step 5 you write “By looking at the code above, you will notice that we’ve injected the new service “placesPhotosService” into the controller so we can issue HTTP Get request.”
However, I don’t see that at all. If I look at the github for the PlacesExplorerController, I see that placesPhotoService is injected. But the most recent code snippet from the controller (in Step 2) only has the placesExplorerService injected.
I don’t know if you’d rather add a snippet in Step 5 or update Step 2, but in any case, it is a bit confusing.
Thanks for your feedback, I will try to rewrite this paragraph, sometimes it is hard to mention every tiny bit.
Similarly, you might mention adding $modal to the injection in the controller, as well as the script tags for placesPhotosService.js and placesPhotosController.js in index.html. Eventually I figured that out, but for one learning how all this works, it’d be nice to have it more explicitly. FWIW, I’m following the tutorial copying & pasting the snippets rather than using the entire finished github repo.
Good catch, it is always better to have the finished repo next to you as a reference, sometime it is hard to cover every tiny detail.
Glad that you are leaning from this tutorial. Let me know if you need further help or if you have any feedback.
Will do. So far it’s been great.
hello tjoudeh, its a really nice tut.. helped me a lot… only one question.
How to make Vanue_Id dynamic. i mean in foursqure api if i put a specific venue Id it gives me corresponding pictures. but how to implement the thing that if i click in any venue it will give me the corresponding pictures??
thanks anyway.
Hello Nabarag,
Glad you liked the tutorial.
If i didn’t understand you wrong, you can’t get venue pictures without passing the venue_id. With each request to foursquare API you need to specify the venue identifier which is the ID in our case.
Currently when you click on any venue the id will be passed to foursquare and the pictures are displayed.
Hope this answers your question, let me know if you need further details.
Excellent. Just getting into angular, this is quite possibly the best and clearest tutorial I have come across.
Glad to hear this Hiram 🙂 Angular is the JS framework. You will love it more when you get deeper!
This is exactly what I have been looking for as regards pagination, the only issue is that when I request for a new page the UI isn’t updating. What do you think might be the error…
What do you use for the front end? As long it is returning paginated data correctly then the issue on how you bind it.
I’m using angularjs mixed with html in my front-end
Well it’s hard to debug it like this, you can prepare a gist so I can check it.
This is my first time of creating a gist, i don’t know if you can work with this https://gist.github.com/kenny007/9991470
Thanks.Got it working, by using a breakpoint i discovered that the page requested wasn’t being sent to the controller so it kept returning the default page=0.
But a little puzzle is that I didn’t use this line at all
var offset = ($scope.pageSize) * ($scope.currentPage – 1);
I just passed in the $scope.currentPage and $scope.pageSize directly as query parameters like this
placesExplorerService.get({ near: $scope.exploreNearby,
query: $scope.exploreQuery,
page: $scope.currentPage ,
pageSize:$scope.pageSize }
Can you define in simply words please what “offset” is and what is the downside to my approach?
Hello Kehinde,
The offset is related to foursquare api only, check there api endpoint here: https://developer.foursquare.com/docs/venues/explore
There is query string param called “offset” where you can calculate the offset by multiplying the page size by current page index, in other words this is the number of records I want to skip.
Oh I see, thanks a bunch..
In “Step 5: Add modal view to view places images” you don’t mention that you must inject the $modal object into the placesExplorerController. Typically not a big deal, but still worth mentioning for those that follow tutorials to the letter.
Hello Mike,
You are right, this has been clarified for follow readers, thank you.
Really liking this tutorial so far, so please don’t see my comments as being overly critical. I just want to help out those that will be following it. So far I’m really liking what Angular has to offer based on this tutorial.
Anywho, there needs to be a comma between placesExplorerService and $filter in the code snippet.
‘use strict’;
app.controller(‘placesExplorerController’, function ($scope, placesExplorerService $filter) {
Hello Mike,
I’m glad that you liked the tutorial, and I want to thank you for you constructive comments, without the feedback from the readers, It is really hard to have post with zero pitfalls. This has been updated.
Good catch by the way, have you used to work as SW quality engineer before? 😉
There’s some interesting auto-magic goo happening between the showVenuePhotos function and the placesPhotosController. It would be cool if this tutorial would describe what’s going on there. It’s easy enough to see what’s going on if you open up the finished project from github, but it would be preferable to have that code and an explanation of that code in the tutorial.
Great work. In a live application, it feels like it wouldn’t be a very good idea to leave your SecretKey for a third-party api stored in the client JS file. How would you recommend handling that in a more secure way? And/or what are pros/cons to creating a Web API to wrap the Foursquare API? Do you have any good example/references for the latter approach? Thanks!
Hi Mike,
I agree with you, there is no “secure” way to store client credentials on JS application, most of the external applications when you registr with them ask you from where the requests are orginating, so they check the URL referrer and I believe they will reject the call if it is coming from different domain. Check this URL for more details.
I’m not big fan of creating a wrapper over an established API, I feel you will be redoing their established API and will end up with an API that is not so polished.
Hope this answers your question.
Thanks for this interesting post.
I have copied your code into my personal computer to follow your steps. I have found two problems:
– First, my pagination directive needs different parameters:
<div data-pagination="" ng-model="currentPage" data-previous-text="” data-first-text=”<>” ng-change=”pageChanged(currentPage)” data-total-items=”totalRecordsCount” data-boundary-links=”true” class=”pagination pagination-sm” data-max-size=”5″ data-rotate=”false” data-items-per-page=”10″>
(please, pay attention to ng-change and ng-model attributes)
– Second, I have changed the argument to the parameter templateUrl to call the modal window:
var modalInstance = $modal.open({
templateUrl: ‘views/placesphotos.html’,
… the rest is the same …
Thanks again
i am new to AngularJs, can i have database structure to understand the application in a better way.
Thanks in Advance
Naresh
Well written article…easy to understand for someone beginning SPA in AngularJS…Thank you
Happy to help 🙂
Hello Taiseer, saw this article over a year ago and still refer to it till today.
I wanna know if I can build on this for a mobile app? Permission, please 🙂