Category: AngularJS
Validating Start/End dates in AngularJS
In this scenario, the user is prompted for starting/ending dates. Ideally these are validated as the user is filling out the form (rather than on submit). However the co-dependence between different fields makes directive use difficult. I was interested in a simple fix. So sorry, this is not too portable. But it’s simple and works.
I’m using the Angular Bootstrap date picker control.
Note that the way I’ve set this up, it piggybacks on my date validation method. See my separate post about this. If you don’t use this method, you’ll need to choose some other way to make sure both dates are valid before comparing them!
In the controller:
$scope.$watch('model.Template.StartDate', validateDates); $scope.$watch('model.Template.EndDate', validateDates); function validateDates() { if (!$scope.model) return; if ($scope.form.startDate.$error.invalidDate || $scope.form.endDate.$error.invalidDate) { $scope.form.startDate.$setValidity("endBeforeStart", true); //already invalid (per validDate directive) } else { //depending on whether the user used the date picker or typed it, this will be different (text or date type). //creating a new date object takes care of that. var endDate = new Date($scope.model.Template.EndDate); var startDate = new Date($scope.model.Template.StartDate); $scope.form.startDate.$setValidity("endBeforeStart", endDate >= startDate); } }
<form name="myForm"> <input id="startDate" type="text" valid-date datepicker-popup="MM/dd/yyyy" ng-model="model.Template.StartDate" name="startDate" ng-required="true" /> <input id="endDate" type="text" valid-date datepicker-popup="MM/dd/yyyy" ng-model="model.Template.EndDate" name="endDate" ng-required="true" /> <span ng-show="form.startDate.$error.endBeforeStart">End date must be on or after start date.</span> <span ng-show="form.startDate.$error.invalidDate || form.endDate.$error.invalidDate">Check dates for validity</span> <span ng-show="form.startDate.$error.required || form.endDate.$error.required">A required date is missing</span> </form>
Another thing you might notice is that we (arbitrarily) attach the error to the start date. For this purpose, it doesn’t matter where we put the error, as long as it’s on the form somewhere. You can’t attach an error directly to the form itself, but an error on any field will invalidate the form.
Angular Bootstrap Date Picker validation fix
The Angular Bootstrap date picker is very helpful for UI look/feel. But its validation is inadequate.
- Javascript is used to determine the date. So I enter 1/1/1, it’s parsed as a valid date even though it’s likely a user error.
- Any errors (for example, the user enters “xyz”) are not reflected in the control $errors, so the form is still considered valid.
Fortunately, we can solve both of the above problems with a simple directive.
//designed to work with the angular bootstrap date control. //sets an error invalidDate when user types the date. .directive('validDate', function () { return { restrict: 'A', require: 'ngModel', link: function (scope, element, attrs, control) { control.$parsers.push(function (viewValue) { var newDate = model.$viewValue; control.$setValidity("invalidDate", true); if (typeof newDate === "object" || newDate == "") return newDate; // pass through if we clicked date from popup if (!newDate.match(/^\d{1,2}\/\d{1,2}\/((\d{2})|(\d{4}))$/)) control.$setValidity("invalidDate", false); return viewValue; }); } }; })
My HTML looks like this:
<form name="myForm"> <input valid-date datepicker-popup="MM/dd/yyyy" type="text" ng-model="myStartDate" class="form-control" name="startDate" ng-required="true" /> <span ng-show="myForm.startDate.$error.invalidDate">Invalid start date.</span> <span ng-show="myForm.startDate.$error.required">Start date is required.</span> </form>
The regex (hard coded into my directive) should match the one in the date picker control, but that is not required. The one I’m using limits the user to slashes / between components. It’s also a bit limited. 99/99/9999 is still a valid value. But you can get as sophisticated as you want here.
Waiting indicator using AngularJS interceptors
There are a number of needs that routinely come up when putting together a web UI. This indicator notifies the user that action is occurring in the background, and goes away automatically when everything is done.
This happens automatically when your AngularJS app is requesting information via http. This is similar to other code on the web, but is modified it to suit the following needs:
- Displays wait indicator automatically when http requests are initiated.
- Tracks the number of requests, and dismisses the indicator when all are resolved.
- A delay of one second prevents indicator from displaying during angular loading views and quick requests.
- No jquery (yay)
If you’re looking for code (and you probably are), I recommend taking a look at the JSFiddle link above – it integrates the code, css and html.
We’re using several key AngularJS features here:
Directive
The directive is Angular’s way to hook UI elements to javascript functionality. Much of the logic is located here.
myApp.directive("loadingIndicator", function (loadingCounts, $timeout) { return { restrict: "A", link: function (scope, element, attrs) { scope.$on("loading-started", function (e) { loadingCounts.enable_count++; console.log("displaying indicator " + loadingCounts.enable_count); //only show if longer than one sencond $timeout(function () { if (loadingCounts.enable_count > loadingCounts.disable_count) { element.css({ "display": "" }); } }, 1000); }); scope.$on("loading-complete", function (e) { loadingCounts.disable_count++; console.log("hiding indicator " + loadingCounts.disable_count); if (loadingCounts.enable_count == loadingCounts.disable_count) { element.css({ "display": "none" }); } }); } }; });
Interceptors
Allows us to tie into http-related events and run our code at that time. In this case we are hooking into both the request, response and responseError (just in case). Here’s another post on interceptors.
myApp.config(function ($httpProvider) { $httpProvider.interceptors.push(function ($q, $rootScope) { return { 'request': function (config) { $rootScope.$broadcast('loading-started'); return config || $q.when(config); }, 'response': function (response) { $rootScope.$broadcast('loading-complete'); return response || $q.when(response); }, 'responseError': function (rejection) { $rootScope.$broadcast('loading-complete'); return $q.reject(rejection); } }; }); });
Broadcast
Broadcast is the way we can fire (and respond to) our own events. As you can see, the interceptor events use $broadcast messages to trigger the logic (located in the directive) to show or hide the indicator.
You can implement this solution from the code on this page, if you include the following:
myApp.factory('loadingCounts', function () { return { enable_count: 0, disable_count: 0 } });
See it in action: Here it is on JSFiddle
AngularJS Interceptors for logging service calls
I should start a series of angularJS posts called “stupid angularJS tricks”, because I’m always trying to figure out how to do this or that. Thank goodness for other peoples’ blogs!
In my project, I’m building out a suite of apps which (true to AngularJS form) get their data from services. It’s handy to log all service calls, to make sure caching is working, etc. Ultimately there will be quite a few service calls, so we may want to keep track of them, and it’s certainly handy for debugging.
Prior to my discovery of interceptors, I would write an entry similar to this just before every $http.get or $http.post call, similar to this insanely simplified example:
console.log('WS GET service ' + svcUrl); $http.get(svcUrl). success(function (result, httpstatus, headers) { });
Although this does the job, it’s not foolproof – I might forget to include it into future code (resulting in an unlogged call), or worse, I may change my get to a post and the log message would be wrong.
Enter the interceptor.
This lovely mechanism allows us to “intercept” an http call and run our custom code at key points. In this case I want to log something prior to running any $http call. There are various other places I can plug in. It’s all here (search on “interceptor”).
Design your factory for injection
In this case, I’m just plugging into the “request” event, which will fire before the request actually occurs. We receive a configuration object which describes the request, and I fashioned a very crude filter. Why? As I discovered, all requests from angular to include views also come through here. I don’t want those logged – just my web service requests.
myapp.factory('serviceLogger', function () { return { request: function (config) { //weed out loading of views - we just want service requests. if (config.url.indexOf('html') == -1) { console.log("HTTP " + config.method + " request: " + config.url); } return config; } }; });
The interceptor above will do its work prior to the request being made. There are 4 places where you can use this method to intercept an HTTP request (from the documentation):
- request: (as above) Interceptor gets called with http config object. The function is free to modify the config object or create a new one. The function needs to return the config object directly, or a promise containing the config or a new config object.
- requestError: Interceptor gets called when a previous interceptor threw an error or resolved with a rejection.
- response: interceptors get called with http response object. The function is free to modify the response object or create a new one. The function needs to return the response object directly, or as a promise containing the response or a new response object.
- responseError: Interceptor gets called when a previous interceptor threw an error or resolved with a rejection. Good place for global handling of exceptions thrown in web services!
If you implement requestError or responseError, be sure to return a $q rejection – otherwise the code in your controller will not recognize the error.
Add the functionality to the $httpProvider
Now you’ve got your interceptor – but you still need to make sure your app is using it! Note that you can only do this in the config section – this is where the $httpProvider is available.
myapp.config(function ($routeProvider, $httpProvider) { //you probably have some sort of a routing table here. $routeProvider .when('/something', { templateUrl: 'views/something.html', controller: 'somethingCtl' }) .when('/anotherthing', { templateUrl: 'views/anotherthing.html', controller: 'anotherthingCtl' }) .otherwise({ redirectTo: '/something' }); //here's where you add your interceptor $httpProvider.interceptors.push('serviceLogger'); })
That’s it!
Be sure to check out all the other stuff that interceptors can do!
Securely Transmitting Information Across AngularJS to .NET Web API Services
Working with angularJS and .NET WebAPI 2 presents some challenges. In this scenario, the client side of our app is a single-page rich java client application written in AngularJS, and makes heavy use of web services. The web services are written in WebApi. Ultimately this app will be a SharePoint 2013 provider hosted app (and appear in an IFrame in SharePoint).
Here’s the issue: Our internal data retrieval process requires user information (i.e. Login ID), but the web services (although the are protected by forms authentication) are not aware of WHO the user is. We need a way to securely pass the user ID whenever a web service request is made.
You can do this by saving user information to a client side cookie. Information is encrypted, so it’s not possible for one user to impersonate another. The encryption key is located on the server.
Note: AES 256 bit Encryption code is needed to implement this. I got mine from this excellent article.
The logic works as follows:
Initial page load:
- Step A: Login info is detected, and user information is determined (ASP Page codebehind).
- Step B: User information is encrypted and saved to a client-side cookie (ASP Page codebehind).
Web service request:
- Step C: AngularJS picks up cookie and passes it in request header.
- Step D: On server side, the user token is picked up from the header and validated (using a FilterAttribute). note: so it applies to all.
- Step E: User token is decrypted, and can be used internally for data retrieval etc.
Steps A and B: User Authentication and Encryption
When the initial page loads, you’ll want to know if the user is logged in. The default app (when setting up in Visual Studio) comes with the wiring necessary to determine the SharePoint user. In my case there’s a third-party authentication provider, but the logic here is the same.
public clientSideUserToken; protected void Page_Load(object sender, EventArgs e) { bool isLoggedIn = false; string userName = null; //Authentication Provider logic for determining login. //SharePoint logic illustrated here. var spContext = SharePointContextProvider.Current.GetSharePointContext(Context); if (spContext != null) { using (var clientContext = spContext.CreateUserClientContextForSPHost()) { if (clientContext != null) { Microsoft.SharePoint.Client.User spUser = clientContext.Web.CurrentUser; clientContext.Load(spUser, user => user.Title); clientContext.ExecuteQuery(); isLoggedIn = true; userName = spUser.Title; } } } if (isLoggedIn) { string token = EncryptUsername(Session, userName); //drop cookie for angular to pick up. string cookieName = "MYCOOKIE"; Response.Cookies[cookieName]["USERTOKEN"] = token; Response.Cookies[cookieName].Expires = DateTime.Now.AddMinutes(1); } else { //HANDLE USER NOT LOGGED IN. } } private string EncryptUsername(HttpSessionState session, string userName) { byte[] byUserName = Encoding.ASCII.GetBytes(userName); byte[] byEncryptedUserName = AESEncryption.AES_Encrypt(byUserName, Config.CookieEncryptionKey); string encryptedUserName = Convert.ToBase64String(byEncryptedUserName); return encryptedUserName; }
Step C: AngularJS Submits User Information Header to Services
In this step, AnglularJS picks up the cookie dropped by the server on the initial page load (see step B), and puts it into the header of the next request.
This code is best located in an AngluarJS Interceptor (read more about interceptors here). The following ‘request’ function will execute every time the user makes a service request.
MyModule.factory('serviceLogger', function ($q, $rootScope, serviceErrorHandler, globalData, $cookies, $cookieStore) { return { 'request': function (config) { //pick up the asp cookie if present, pull out the encrypted user token, and put it into the header. var aspcookie = $cookies["MYCOOKIE"]; if (aspcookie) { var jsonCookie = {}; var items = aspcookie.split("&"); for (var i = 0; i < items.length; i++) { var splitPos = items[i].indexOf('='); jsonCookie[items[i].substring(0, splitPos)] = items[i].substring(splitPos + 1); } config.headers.HEADERTOKEN = jsonCookie.USERTOKEN; } return config; }, };
Steps D and E: Add a Custom Authentication Filter into WebApi to Check for Valid User
Finally, the header from step C is decoded and validated. If validation fails, the user gets a HTTP 401 (forbidden) error.
Using WebAPI, you can write code which can intercept all requests. To do this, write a custom authenticator implementing IAuthenticationFilter. Then apply it to ALL web services using the following code in your Global.asax:
protected void Application_Start(object sender, EventArgs e) { //whatever else you've got here GlobalConfiguration.Configuration.Filters.Add(new ValidateUserToken()); }
IMPLEMENTATION NOTE: tO implement the IAuthenticationFilter, I used an abstract filter to do some of the heavy lifting. I found a great example of that here. Another great help was this blog, which provides a straightforward way to implment IHttpActionResult. This implementation even allows us to send a custom object back to the client, along with the appropriate HTTP code!
Here’s an example of that class:
public class ValidateUserToken : AAuthenticationFilterAttribute { public override void OnAuthentication(System.Web.Http.Filters.HttpAuthenticationContext context) { try { DecryptUsername(context.Request); } catch { AuthResult msg = new AuthResult() { Message = "Server rejected the request. Authorization invalid or expired." }; context.ErrorResult = new SimpleHttpResult<AuthResult>(context.Request, HttpStatusCode.Forbidden, msg); } //otherwise pass through and everything is ok. } //note (step E): if you make this method static, you can retrieve the user name at will. private static string DecryptUsername(HttpRequestMessage request) { string encryptedUserName = request.Headers.GetValues("HEADERTOKEN").First(); byte[] byEncryptedUserName = Convert.FromBase64String(encryptedUserName); byte[] byUserName = AESEncryption.AES_Decrypt(byEncryptedUserName, Config.CookieEncryptionKey); string userName = Encoding.ASCII.GetString(byUserName); return userName; } } //Very simple result object. Any properties added here will be passed to the client side, as JSON. public class AuthResult { public string Message; }
Once these methods are in place, no further code modifications are necessary, and we can now write services and use them with confidence.
SharePoint App Part Resizing, When Using AngularJS
Provider Hosted Apps
Sometimes it’s necessary to embed rich, non-SharePoint functionality in a SharePoint site. The preferred way to do this in SharePoint 2013 is to use a Provider Hosted App. SharePoint then acts as a “host” for the app, which can function independently. This gives the ability to layer in complex business rules, perform development cycles AWAY from SharePoint, and then embed the functionality in the site.
IFrame Presentation
Taking this route, one challenge is to ensure that the app appears as part of the page. Provider Hosted Apps are hosted in an iframe, but users need not be aware of this fact. If not done properly, parts of the app might disappear or for scroll bars may appear in the frame. Generally the iframe content cannot control the size of the “parent” window.
So it’s up to the developer to ensure that the iframe is sized properly for the app. Fortunately, Microsoft has anticipated this issue and given us some javascript “hooks” to do this. There are a number of blogs on how to achieve this, from simple implementation to complex ones . Bottom line, your hosted javascript code needs to send a message similar to this one to the parent window:
<message senderId=12345ABCD>resize(100%,800px)</message>
Additional Challenges with AngularJS
Resizing the parent iFrame is a simpler task with a “traditional” (mostly server side) app – then page size changes when the page reloads, and the necessary logic can be run then.
But with AngularJS (a single page app), the iframe needs to resize when we move from one “view” to another, or the page size changes due to the underlying javascript logic. When resizing, we need to know what the size of our content will be. Fortunately, this is not rocket science – it just requires a “harmonic convergence” of code.
Step 1: Add wrapper HTML
Identify or create a wrapper div which will contain all HTML in your app. We will check this wrapper to determine how large the iframe should be.
<body ng-app="appName"> <div main-wrapper> <!-- Add your site or application content here --> <div ng-view=""></div> <div> </body>
Step 2: Add Resizing Code
Add some javascript code to resize the iframe. The following code assumes that only the height of your app will vary. Note the chrome fix – this is another issue that applies only to Angular implementations.
"use strict"; function adjustFrameSize(contentHeight) { var senderId, resizeMessage = '<message senderId={Sender_ID}>resize({Width}, {Height}px)</message>'; var args = document.URL.split("?"); if (args.length < 2) return; var params = args[1].split("&"); for (var i = 0; i < params.length; i = i + 1) { var param = params[i].split("="); if (param[0].toLowerCase() == "senderid") { senderId = decodeURIComponent(param[1]); senderId = senderId.split("#")[0]; //for chrome - strip out #/viewname if present } } var step = 30, finalHeight; finalHeight = (step - (contentHeight % step)) + contentHeight; resizeMessage = resizeMessage.replace("{Sender_ID}", senderId); resizeMessage = resizeMessage.replace("{Height}", finalHeight); resizeMessage = resizeMessage.replace("{Width}", "100%"); console.log(resizeMessage); window.parent.postMessage(resizeMessage, "*"); }
Step 3: Add an Angular Directive
In your AngularJS code, create a directive which will attach to your wrapper div. What you’re doing here is creating an event which can be fired at any time from your app, to trigger a resize of the iframe based on the height of your wrapper div.
.directive('mainWrapper', function ($timeout) { return { restrict: 'A', link: function (s, e, attrs) { s.$on('resizeframe', function () { $timeout(function () { //timeout ensures that it's run after the DOM renders. adjustFrameSize(e[0].offsetHeight); }, 0, false); }); } }; })
Step 4: Trigger the Resizing Event
Finally, you need to instruct the event to fire at the appropriate times. This might be when a controller is intially accessed, or perhaps when data are accessed. The strength of this method is that you can control when resizing occurs. You need to inject $scope and use $broadcast. Here are a couple of examples:
.controller('FirstPageControler', function ($scope) { //resize frame when controller loads $scope.$parent.$broadcast('resizeframe'); }) .controller('AnotherControler', function ($scope, webServices) { webServices.reports().then(function (response) { $scope.reports = response; //resize frame upon successful return from a service. //presumably we have returned with data that will resize the page. $scope.$parent.$broadcast('resizeframe'); }, function (response) { $scope.reports = response; }); })
That should do the job! Good luck, and feel free to leave a comment if you’ve found a better way.
Android Notifications using Cordova and Ionic
So you want to add push notifications to your Cordova/Android/Ionic app? I’ll walk through it. Most of this entry applies to Cordova and Android, in case you’ve made the mistake of NOT using Ionic…
Basic Info
Android notifications work through Google GCM (Google Cloud Messaging). You can read the details at http://developer.android.com/google/gcm/index.html, but here’s the summary:
Here’s the narrative version: When your application starts up, the Android device registers with GCM server, which responds with a Registration ID. The device then sends the registration ID to our Message Server, which stores it for future use. Other information, such as the user’s name or location, can be sent for message targeting.
When we want to send out a notification, our Message Server sends the text to the GCM server, along with the registration ID and the API . The GCM server relays info to the device.
The GCM Server
Setting up your Google GCM server should be a fairly quick process. It’s a simple mechanism from our perspective, but does a lot of the heavy lifting for us. Follow these directions to get it running: http://developer.android.com/google/gcm/gs.html. Be sure to make note of your GCM Project ID, and your API Key. The Project ID will go into your Android app configuration, for registration. The API Key is used to send notifications.
The Notification Server
There are any number of ways to do this, and from what I’ve seen, folks often use custom code for this. Of coruse there are some off the shelf products. But for development, I recommend the node-gcm project https://github.com/ToothlessGear/node-gcm. It’s a super-simple way to communicate with the GCM server. First install
npm install node-gcm
Next set up a node script similar to this one:
var gcm = require('node-gcm'); var message = new gcm.Message(); //API Server Key var sender = new gcm.Sender('INSERT_YOUR_API_SENDER_KEY_HERE'); var registrationIds = []; // Value the payload data to send... message.addData('message', "Hello Cordova!"); message.addData('title','Push Notification Sample' ); message.addData('msgcnt','2'); // Shows up in the notification in the status bar message.addData('soundname','beep.wav'); //Sound to play upon notification receipt - put in the www folder in app message.collapseKey = 'demo'; message.delayWhileIdle = true; //Default is false message.timeToLive = 3000;// Duration in seconds to hold in GCM and retry before timing out. Default 4 weeks (2,419,200 seconds) if not specified. // At least one reg id required registrationIds.push('THIS_IS_THE_REGISTRATION_ID_THAT_WAS_GENERATED_BY_GCM'); /** * Parameters: message-literal, registrationIds-array, No. of retries, callback-function */ sender.send(message, registrationIds, 4, function (err, result) { console.log(result); });
The Android App
As you know by now, you need a plugin to do just about anything in Cordova. So install the PushPlugin https://github.com/phonegap-build/PushPlugin. This should be the only plugin you need
for both Android and iOS. You might want to scan the instructions, but (as of now) they are both overly complicated and incomplete. For starters, you don’t need PlugMan to install. Just use:
cordova plugin add https://github.com/phonegap-build/PushPlugin.git
The coding was a little bit tricky, but I finally managed a functional script for Ionic notifications. See inline comments for more information on what’s going on here:
//factory for processing push notifications. angular.module('pushnotification', []) .factory('PushProcessingService', function() { function onDeviceReady() { console.info('NOTIFY Device is ready. Registering with GCM server'); //register with google GCM server var pushNotification = window.plugins.pushNotification; pushNotification.register(gcmSuccessHandler, gcmErrorHandler, {"senderID":gcmAppID,"ecb":"onNotificationGCM"}); } function gcmSuccessHandler(result) { console.info('NOTIFY pushNotification.register succeeded. Result = '+result) } function gcmErrorHandler(error) { console.error('NOTIFY '+error); } return { initialize : function () { console.info('NOTIFY initializing'); document.addEventListener('deviceready', onDeviceReady, false); }, registerID : function (id) { //Insert code here to store the user's ID on your notification server. //You'll probably have a web service (wrapped in an Angular service of course) set up for this. //For example: MyService.registerNotificationID(id).then(function(response){ if (response.data.Result) { console.info('NOTIFY Registration succeeded'); } else { console.error('NOTIFY Registration failed'); } }); }, //unregister can be called from a settings area. unregister : function () { console.info('unregister') var push = window.plugins.pushNotification; if (push) { push.unregister(function () { console.info('unregister success') }); } } } }); // ALL GCM notifications come through here. function onNotificationGCM(e) { console.log('EVENT -> RECEIVED:' + e.event + ''); switch( e.event ) { case 'registered': if ( e.regid.length > 0 ) { console.log('REGISTERED with GCM Server -> REGID:' + e.regid + ""); //call back to web service in Angular. //This works for me because in my code I have a factory called // PushProcessingService with method registerID var elem = angular.element(document.querySelector('[ng-app]')); var injector = elem.injector(); var myService = injector.get('PushProcessingService'); myService.registerID(e.regid); } break; case 'message': // if this flag is set, this notification happened while we were in the foreground. // you might want to play a sound to get the user's attention, throw up a dialog, etc. if (e.foreground) { //we're using the app when a message is received. console.log('--INLINE NOTIFICATION--' + ''); // if the notification contains a soundname, play it. //var my_media = new Media("/android_asset/www/"+e.soundname); //my_media.play(); alert(e.payload.message); } else { // otherwise we were launched because the user touched a notification in the notification tray. if (e.coldstart) console.log('--COLDSTART NOTIFICATION--' + ''); else console.log('--BACKGROUND NOTIFICATION--' + ''); // direct user here: window.location = "#/tab/featured"; } console.log('MESSAGE -> MSG: ' + e.payload.message + ''); console.log('MESSAGE: '+ JSON.stringify(e.payload)); break; case 'error': console.log('ERROR -> MSG:' + e.msg + ''); break; default: console.log('EVENT -> Unknown, an event was received and we do not know what it is'); break; } }
Call it from here:
app.run(function(PushProcessingService) { //run once for the app PushProcessingService.initialize(); });
Hopefully this is helpful to someone!
Thanks to these folks (and others) who put together helpful blogs on this topic:
Cordova and AngularJS – Opening links in system browser
In the course of writing my Android/Cordova/Ionic app, I ran into another tricky requirement: Display HTML content from a web service, containing links. These links should open in the user’s preferred browser. In the end, there are several steps necessary to getting this right.
Step 1: Display unescaped HTML in the app.
When AngularJS binds content, special characters are “escaped”, so HTML tags are visible and of course non-functional. Fortunately AngularJS has a fix for this: ng-bind-html. Use this instead of ng-bind (which is equivalent to the double-bracket method). Your content will be slightly sanitized (which will be a problem later), but the links are preserved.
<div ng-bind-html="content.Description"></div>
At this point you’ll see some strange behavior: the links open inside webkit. So the user stays in your app, but there is no way to go back to your Angular HTML5 pages. In my case, even the back button did not work!
As with so many Cordova issues, “there’s a plugin for that.”
Step 2: Install the inappbrowser plugin
Despite its name, the inappbrowser plugin enables the behavior we’re looking for – opening a browser window EXTERNAL to our application. The following command will install it:
cordova plugin add org.apache.cordova.inappbrowser
In theory, all you have to do is set up your links as follows:
window.open('http://intown.biz', '_system', 'location=yes');
Step 3: Apply the “inappbrowser” approach (part 1).
The next question is how to apply this to your downloaded content? Because of AngularJS, there are two issues:
- JavaScript is “sanitized” (i.e. not rendered) by ng-bind-html. Otherwise, we would be able to write a filter to replace our ordinary Anchor tags with tags that invoke the javascript you see above.
- I tried directives, but since I don’t have control over the HTML being passed in, that proved more complicated. Depending on your circumstances, a directive may work for you.
I finally arrived at a two step process. First, use a simple custom filter to add ‘class=”ex-link”‘ to each section where we want the links to open in the external browser.
.filter('externalLinks', function() { return function(text) { return String(text).replace(/href=/gm, "class=\"ex-link\" href="); } })
The above filter would need to be more sophisticated if there are classes within the HTML that need to be preserved. Applying the filter in AngularJS, your HTML now looks like this:
<div ng-bind-html="content.Description | externalLinks"></div>
Step 3: Apply the “inappbrowser” approach (part 2).
Finally, we address the question of how to force our links to open in the _system window using JavaScript. For this we’ll resort to JQuery. I generally like to keep JQuery out of my projects, but in my case it was already included for a third-party tool. If anyone knows of a “pure Angular” approach to this, I’m all ears.
Using $timeout ensures that this handler is one of the last applied to the page.
$timeout(function () { $('.ex-link').click(function () { var url = $(this).attr('href'); window.open(encodeURI(url), '_system', 'location=yes'); return false; }) })
You might place the above code into the controller of the page that needs it, or in a factory containing utility functions. Don’t forget to inject $timeout in the appropriate place.
This two-step approach has advantages: obviously the “ex-link” class can be added directly to a template’s HTML, allowing you to place an “external link” anywhere in your app, and distinguish from your app’s internal links.
Of course there are some risks to displaying foreign HTML in a Cordova app. For my project, I have been assured that the HTML will be correct, but of course if it’s not, my app could break. So down the road I may add HTML integrity checking, but in the Agile tradition, I will only address this if it’s a real issue.
AngularJS/Cordova mobile app Dev Setup – Part 2
In AngularJS/Cordova mobile app Dev Setup – Part 1 we got things rolling. We now have node.js, Cordova and associated tools in our environment. Now to apply AngularJS and Ionic.
Part 1 will get you up and running with Cordova, from scratch.
Part 2 adds AngularJS and Ionic to the mix
Part 3 running your app
Ionic is a package of components including AngularJS, custom Angular directives, and some styles which give our HTML5 nice “mobile” look. Out of the box, I found that my app was fairly true to the “iphone look”. Although I’m writing an Android application, this app might eventually be used for iPhone, so this suits me fine.
Ionic
Ionic is very simple to get started. We’re just going to assume that you followed the steps in part 1.
- Install Ionic
-
npm install -g ionic
- Create an Ionic app
ionic start myApp
Now you’re ready to run the sample code. This can be done using Cordova to deploy to the emulator or your phone. This code is also simple enough that you can also run it in a browser!
You can use Express or another lightweight brower to do this, by opening up the www directory [TODO: more detail].
Going the next step
You now have a start on your project, but there are still some tools that you will probably need. I ended up adding these tools shortly after creating it, so here they are:
- Create a package.json file.
- Grunt, a scripting framework for node.js. First you’ll need the command line (installed gloablly), and then install to the project (saving to json file)
npm install -g grunt-cli npm install grunt --save-dev
- Bower, a packaging manager, similar to npm but intended to manage external tools used in your app, rather than in your dev environment.
npm install -g bower
Many bower packages require you to install git as well (for example, angular-google-maps). When installing for windows, I selected the “add to path” option. This makes it possible to run bower from a standard MS-DOScommand line (you don’t have to use git/bash).
Enjoy Angular
Now it’s time to poke around with Angular and add some functionality. Good luck!
AngularJS/Cordova mobile app Dev Setup – Part 1
The Cordova framework (pretty much synonymous with PhoneGap) puts an HTML5 “wrapper” over the native code (for ios or Android), working toward the goal of write-once-run-anywhere code. Instead of writing in Objective C or Java, most of the code is written in HTML5 and Javascript. I’ve been using AngularJS on a couple of other projects, so naturally I wondered if anyone had applied it to mobile/Cordova/PhoneGap applications. Sure enough I found Ionic, a framework composed of AngularJS, custom Angular directives, and some styles which make things go together nicely.
Part 1 will get you up and running with Cordova, from scratch.
Part 2 adds AngularJS and Ionic to the mix
Part 3 running your app
My first task was to set up my environment. Sure, all the tools were out there, but I spent more time piecing things together than I would have liked. It would have helped me if there had been a Getting Started guide for a first-time mobile developer. So here goes…
I use a Windows 8 machine, so it’s fortunate that I’m designing primarily an Android app. It seems that a Mac is the best choice because you can develop to any mobile platform on it. But I’ll try to keep my development software platform agnostic so that an ios dev could grab the code and use it.
My approach was to get first get Cordova set up and tested, and then apply the AngularJS scaffolding.
Generic Cordova Dev Environment Setup
Install
- node.js – this includes the npm package manager, and is the gateway to a world of open source tools and packages
- Java JDK – don’t laugh, but this was not on my machine yet. Hey, it’s a new Windows 8! Make sure to download the Java Development Kit (JDK). The JRE does not have development tools.
- Apache ANT – A scripitng framework http://ant.apache.org/bindownload.cgi
- Android Developer SDK – Lower-level stuff for Android. http://developer.android.com/sdk/
- Cordova – Now you’ll start using the package manager you installed with node.js. Open up a command line and run:
npm install -g cordova
Settings
Open up your Computer Properties/Advanced System Settings, then click on Environment Variables. Add the following variables (substitute your own for mine):
ANT_HOME = C:\bin\apache-ant-1.9.3 ANDROID_HOME = C:\bin\adt-bundle\sdk JAVA_HOME = C:\Program Files\Java\jdk1.7.0_51
Then add the following line to the end of the path:
%JAVA_HOME%\bin;%ANT_HOME%\bin;cordo%ANDROID_HOME%\tools;%ANDROID_HOME%\platform-tools
Creating a project
If I haven’t left anything out above (and if you haven’t), you should be ready to use the above tools. If you’re confident and want to go straight to the Angular/Ionic Setup, go for it. Otherwise open a command line and type the following:
> cordova create hello com.example.hello "HelloWorld" > cd hello > cordova platform add android > cordova build
This will create a boilerplate project, a very simple hello world application. The next commands modify the project by adding android support to it, and build it.
A word about IDE’s
The Android Developer SDK comes with the Eclipse editor. This works very nicely for Java, but (because we’ll be using Cordova) we’re going to be working mostly with HTML and Javascript. I chose JetBrains WebStorm, because (despite being a commercial product) it works well for HTML and JavaScript, and extends nicely to AngularJS. Full featured and inexpensive.
Of course I eventually expect the need for a full featured Java editor later in the game. For that I plan to go back to Eclipse.