How to: Opening links in content in system browser instead of Cordova browser wrapper


#1

Hey there,

I’d like to share an implementation with you, partly as documentation for others and maybe to see if it can be improved in some way.

There are some cases in which you might have content that links to an external website. The problem is that projects wrapped in Cordova by default open all links in the wrapper browser, instead of opening them in the system browser.

The way around this is to use a Cordova plugin called inappbrowser. In the documentation it says you can open any link in the system browser by formatting the link like this:

<a href="http://test_url.com" target="_system">Link</a>.

In my experience this doesn’t work: links are still opened in the Cordova wrapper.

Inappbrowser does work however when you open links with window.open(url, '_system'). So you could write a function and tie it to ng-click like so:

<a ng-click="GotoLink('http://test_url.com')">Link</a>,

and then set up a function in your controller (or turn it into a directive):

  $scope.GotoLink = function (url) {
    window.open(link,'_system');
  }

The problem in my case was that I was loading in a piece of html from a CMS in a json file that could contain multiple links and they were in the default format of <a href="link">. I had to find a way to automatically rewrite them to use ng-click. So I wrote a function in my loader module that find-replaced any links in the content.

This lead me to my next problem though. I’m using ngSanitize to display html content by using ng-bind-html. Like so:

<p ng-bind-html="'{{some_variable}}'">

The problem was ng-bind-html automatically filtered out ng-click. So when I used ng-bind-html to display the html content, the ng-click was no longer there on my a element.
I tried using ng-bind-html-unsafe, but that didn’t work either.

In the end I added two directives. The first is by @gregorypratt called browseTo. Any element that contains browse-to is now opened via window.open with target _system, so it opens in the system browser nicely.

That left with me with the problem of ngSanitize removing any angular in it’s output when using ng-bind-html.

So the second directive I added is compile. This keeps browse-To or ng-click in the ouput. So instead of using:

<p ng-bind-html="'{{some_variable}}'">

I now use:

<p compile="some_variable"> // Notice the lack of accolades and quotes!

And this works! In the fields that I choose, links to external sites are now opened in the system browser.

Full code of directives:

angular.module('MyApp.Directives').directive('browseTo', function ($ionicGesture) {
 return {
  restrict: 'A',
  link: function ($scope, $element, $attrs) {
   var handleTap = function (e) {
    var inAppBrowser = window.open(encodeURI($attrs.browseTo), '_system');
   };
   var tapGesture = $ionicGesture.on('tap', handleTap, $element);
   $scope.$on('$destroy', function () {
    // Clean up - unbind drag gesture handler
    $ionicGesture.off(tapGesture, 'tap', handleTap);
   });
  }
 }
})

angular.module('MyApp.Directives').directive('compile', ['$compile', function ($compile) {
  return function(scope, element, attrs) {
    scope.$watch(
      function(scope) {
        return scope.$eval(attrs.compile);
      },
      function(value) {
        element.html(value);
        $compile(element.contents())(scope);
      }
   )};
}])

So this is quite a complicated way of achieving something relatively straightforward as opening a link in the system browser. So I was wondering if anyone else was running into the same problem and found another, perhaps more elegant solution?


Why are ng-click events being stripped out of my data?
Opening External Links in Ionic
#2

Hi coen_warmer,

How funny - Today I created an entry in my blog, detailing how I did this in my project. I can’t say it’s any more elegant - maybe slightly less code. Take a look.

Like you, I’m open to suggestions to make it more elegant.


Ionic - How to intercept dynamic links
#3

Hey @mmendelson, just saw the blog! Looks like we were both banging our heads on the same problems.

I think I like my solution a bit better for now as it doesn’t use jQuery. But your solution has the added benefit that you can still use ng-bind-html.

So yeah, definitely interested in learning how other people are approaching this issue. Maybe something Ionic could provide some support for out of the box…? Certainly seems like a common use case.


#4

I thought about it as well and I think I like the filter approach more. However includig jQuery is not an option. The problem is the missing selector engine in jQLite.


#5

Hey, I’m pretty new to Ionic and Angular and I don’t quite understand how you’re using the first directive. My use case is pretty much identical to yours but I can’t get my setup to work. I’ve got my content returning but do I need to do anything more to write or change the <a> tags in my compiled post content?

Any help would be greatly appreciated.


#6

@Tyler What exactly don’t you understand? Have you successfully integrated both directives in your app?


#7

Apologies, I got it working. Thanks!


#8

Great, happy to hear it!


#9

I don’t know if this is any more elegant, but here is another approach. I Created a directive that overrides the anchor tag, using window.open for external links (begin with “http”). I am using this in conjunction with the “compile” directive provided by @coen_warmer. I am not using Ionic at the moment, but this is working in a Cordova angularjs app. The advantage to this approach is that I can reuse the same static html pages in a context where angular is not available (in this case the user manual for the app).

.directive('a', function () {
  return {
    restrict: 'E',
    link: function (scope, element, attrs) {
      if ( !attrs.href ){
        return;
      }
      var url = attrs.href;
      if ( url.lastIndexOf('http',0) === 0 ){
        element.on('click',function(e){
          e.preventDefault();
          if(attrs.ngClick){
              scope.$eval(attrs.ngClick);
          }
          window.open(encodeURI(url), '_system');
        });
      };
    }
  };
})

#10

@ndudenhoeffer That works great, thanks. You can also use regex to also allow https urls.

app.directive('a', function () {
    return {
        restrict: 'E',
        link: function (scope, element, attrs) {
            if ( !attrs.href ){
                return;
            }
            var externalRe = new RegExp("^(http|https)://");
            var url = attrs.href;

            if(externalRe.test(url)) {
                element.on('click',function(e){
                    e.preventDefault();
                    if(attrs.ngClick){
                        scope.$eval(attrs.ngClick);
                    }
                    window.open(encodeURI(url), '_system');
                });
            }
        }
    };
});

#11

Guys struggled with the same problem today - and thanks for all you help in these threads

I used bits of a few solutions. I like the filter method but not on a class as I have other class stuff defined and also I’m not in control of the html content that can be pushed in. So I changed the filter too:

.filter('externalLinks', function() {
    return function(text) {
        //return String(text).replace(/href=/gm, "class=\"ex-link\" href=");
        //return String(text).replace(/href=/gm, "ng-click=\"exLink()\" href=");
        //
        // NOTE:
        // can't use ng-click as it is not in Angular Land as $sce and ng-bind-html
        // ALSO - must do filters in this order 'content | externalLinks | to_trusted'
        //        so this string stays in content
        return String(text).replace(/href=/gm, "onclick=\"angular.element(this).scope().exLink(this);return false\" href=");
   }
 })

This allows a exLink function in the controller and passes in the link element. The controller then looks like:

$scope.exLink = function (link){
    var url = link.href;
    window.open(encodeURI(url), '_system', 'location=yes');
};

This works well for me. Sure it can be polished but good for now.

Also this is my to_trusted filter that help with html being passed in

.filter('to_trusted', ['$sce', function($sce){
    return function(text) {
        return $sce.trustAsHtml(text);
    };
}])

So your template can look like:

    <div ng-bind-html="condition.Detail | externalLinks | to_trusted"></div>

Hope this makes sense and helps someone else

cheers
Darren


#12

I used jqLite to do the same thing, which I think is easier.


#13

Hey guys,

I’m not sure if this issue is still bothering anyone, but I wrote a solution today that doesn’t require any code in the controller or a directive. It’s purely using ng-bind-html and a filter that substitutes inline javascript. As far as I can tell this can handle untrusted HTML as well.

See the JSFiddle or the gist:


http://jsfiddle.net/sxcjmoj5/3/

myApp.filter('hrefToJS', function ($sce, $sanitize) {
    return function (text) {
        var regex = /href="([\S]+)"/g;
        var newString = $sanitize(text).replace(regex, "onClick=\"window.open('$1', '_blank', 'location=yes')\"");
        return $sce.trustAsHtml(newString);
    }
});

In your html template:

<div ng-app="myApp">
    <div ng-controller="MyCtrl">
         <h1>Before</h1>

        <p ng-bind-html="html"></p>
        <p ng-bind-html="plaintext"></p>
         <h1>After</h1>

        <p ng-bind-html="html | hrefToJS"></p>
        <p ng-bind-html="plaintext | linky | hrefToJS"></p>
    </div>
</div>

Your data bound to the scope:

myApp.controller('MyCtrl', function ($scope) {
    $scope.html = "This a link: <a href='https://www.google.com'>Google</a> :)";
    $scope.plaintext = "This is a link: https://www.google.com :) "
});

You need to include ngSanitize for this, but you can also cut out those sections if you really want to.


#14

I have tested this solution, my compiled code is:

a onclick=“window.open(‘http://google.es’, ‘_blank’, ‘location=yes’)”

But when I click the link in a LG L40 with Android 4.4.2 the url is opened in the app.

Any solution??

Thanks


#15

Sorry, Now is working ok… I need a coffee :stuck_out_tongue:


#16

hello

works fine but my links are not underlined and in blue, any suggestion ?
I works with Ionic framework.

thx a lot


#17

this seemed to work pretty well; my links now open in the device’s default browser; BUT… and this was pretty odd, when i exit the browser and re-open my app; i am taken back to the browser

sorry, not a big fan of how this forum displays threads… i am replying to Sander’s post on using a directive.


#18

thanks rewonc this is the option i went with and it seems to work well. You have 1 small mistake though which is the answer to elepetit’s issue with links not blue. you need to include an href="#" in your A tag, so your code should be:

myApp.filter('hrefToJS', function ($sce, $sanitize) {
    return function (text) {
        var regex = /href="([\S]+)"/g;
        var newString = $sanitize(text).replace(regex, "href=\"#\" onClick=\"window.open('$1', '_blank', 'location=yes')\"");
        return $sce.trustAsHtml(newString);
    }
});

#19

Hi there, can any one help me to use inappbrowser to open my links that comes from here {{many links }}
thanks.


#20

Use ng-repeat to create a loop, in the loop create an a tag with an attribute target as _system, make sure the inappbrowser plugin is installed and put this piece of code in you app.js

// This function allows the user to open links in their external (phone) browser.
.directive('browseTo', function ($ionicGesture) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var handleTap = function (e) {
        // Possible: capture Google Analytics here
        var inAppBrowser = window.open(encodeURI($attrs.browseTo), '_system');
      };
      var tapGesture = $ionicGesture.on('tap', handleTap, $element);
      $scope.$on('$destroy', function () {
        // Clean up - unbind drag gesture handler
        $ionicGesture.off(tapGesture, 'tap', handleTap);
      });
    }
  }
})

You can check which plugin is installed with ‘ionic plugin’