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.
Hi,
Thank you a lot for your article. It was exactly what I needed :)
I have the same problem as you (except I control the served html) but I end up with a quite different solution.
I created two directive : one to render html (not using ng-bind-html) and the other on to open links :
.directive(‘externalContent’, function($compile){
‘use strict’;
return {
restrict: ‘A’,
scope: {
html: ‘=externalContent’
},
link: function(scope, element, attrs){
scope.$watch(‘html’, function(newVal){
element.html(”);
element.append($compile(”+scope.html+”)(scope));
});
}
};
})
.directive(‘href’, function(){
‘use strict’;
return {
restrict: ‘A’,
scope: {
url: ‘@href’
},
link: function(scope, element, attrs){
if(scope.url.indexOf(‘http://’) === 0 || scope.url.indexOf(‘https://’) === 0){
element.bind(‘click’, function(e){
e.preventDefault();
window.open(encodeURI(scope.url), ‘_system’, ‘location=yes’);
});
}
}
};
})
I choose to call I second directive ‘href’ to reduce friction writing external html, so I have to select link to open outside (here, it’s links starting with http:// or https://).
If you prefer adding an attribute, here is the directive :
.directive(‘externalLink’, function(){
‘use strict’;
return {
restrict: ‘A’,
scope: {
url: ‘@href’
},
link: function(scope, element, attrs){
element.bind(‘click’, function(e){
e.preventDefault();
window.open(encodeURI(scope.url), ‘_system’, ‘location=yes’);
});
}
};
})
I hope you enjoy.
Thanks again !
Hi Michel,
great article! It is exactly what i need to parse twitter links. But, I still get the same behaviour.
Looks like my code does not enter the click event.
I did what you said and put the $timeout function in the controller of the page. I can see in Chrome debugger that the filter works. Any thoughts?
regards Bas
Hey Michael,
Cool article! It gave me some inspiration and I think I found a much easier solution. Basically, in step 3 just write the javascript inline. I wrote the regex and accompanying details here:
https://gist.github.com/rewonc/e53ad3a9d6ca704d402e
You can also see the jsfiddle at:
http://jsfiddle.net/sxcjmoj5/3/
Hi Rewon,
That does look like a simpler solution. I look forward to testing this out!
Why you need to wrap the jquery function with a $timeout?
Eldy, using the timeout (even if there’s a 0 millisecond wait on it) ensures that the code will be run AFTER the page loads.
Does you know how to open a link in “_system” browser from a php page within Inappbrowser. This plugin ony Opens all links inside itself even if you definine target as “_system” many thanks i am a newbie to Phonegap
Thanks a lot man. I was taking a rather inelegant approach with unnecessary directives. This one is simpler and less intrusive in my use case. Cheers!
Great article brother.
This was awfully helpful, great work.
Thank you for your article. but I have one problem.
when page load, the work ‘undefined’ appear for a moment.
and then the word disappear, page loaded.
I don’t want to see that word. what should I do?