20

How would I create a directive in angularjs that for example takes this element:

<div>Example text http://example.com</div>

And convert it in to this

<div>Example text <a href="http://example.com">http://example.com</a></div>

I already have the functionality written to auto link the text in a function and return the html (let's call the function "autoLink" ) but i'm not up to scratch on my directives.

I would also like to add a attribute to the element to pass a object in to the directive. e.g.

<div linkprops="link.props" >Example text http://example.com</div>

Where link.props is object like {a: 'bla bla', b: 'waa waa'} which is to be passed to the autoLink function as a second param (the first been the text).

2
  • 1
    is there any reason why you need a directive specific for that? Is there any reason why ngHref isn't an option? (it does accept angular expressions) docs-angularjs-org-dev.appspot.com/api/ng.directive:ngHref Commented Feb 4, 2013 at 18:16
  • So you want to search for URLs in the element's text and create links from them? I'm not sure I understand the second part with the "link props" Commented Feb 4, 2013 at 18:23

6 Answers 6

39

Two ways of doing it:

Directive

app.directive('parseUrl', function () {
    var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?/gi;
    return {
        restrict: 'A',
        require: 'ngModel',
        replace: true,
        scope: {
            props: '=parseUrl',
            ngModel: '=ngModel'
        },
        link: function compile(scope, element, attrs, controller) {
            scope.$watch('ngModel', function (value) {
                var html = value.replace(urlPattern, '<a target="' + scope.props.target + '" href="$&">$&</a>') + " | " + scope.props.otherProp;
                element.html(html);
            });
        }
    };
});

HTML:

<p parse-url="props" ng-model="text"></p>

Filter

app.filter('parseUrlFilter', function () {
    var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?/gi;
    return function (text, target, otherProp) {
        return text.replace(urlPattern, '<a target="' + target + '" href="$&">$&</a>') + " | " + otherProp;
    };
});

HTML:

<p ng-bind-html-unsafe="text | parseUrlFilter:'_blank':'otherProperty'"></p>

Note: The 'otherProperty' is just for example, in case you want to pass more properties into the filter.

jsFiddle

Update: Improved replacing algorithm.

Sign up to request clarification or add additional context in comments.

7 Comments

This is great apart from one thing. What if the text populating the field needs to parsed first e.g. like in this jsfiddle jsfiddle.net/CcrPz
Updated answer. Please check if it helps.
Repetition of the same link creates a problem here. What can be the quickest fix for this?
Use a normalize function to remove duplicated URLs from the urlPattern match result and create a RegExp for global replace of those URLs. Check here
I don't understand why you use a single quote everywhere and then when it comes time to have double quotes in the string decide to change to double quotes to wrap it so you have all this ugly escaping. Why can't JavaScript devs just stick to a single or double quote except when switching will be convenient?
|
5

To answer the first half of this question, without the additional property requirement, one can use Angular's linky filter: https://docs.angularjs.org/api/ngSanitize/filter/linky

Comments

4

The top voted answer does not work if there are multiple links. Linky already does 90% of the work for us, the only problem is that it sanitizes the html thus removing html/newlines. My solution was to just edit the linky filter (below is Angular 1.2.19) to not sanitize the input.

app.filter('linkyUnsanitized', ['$sanitize', function($sanitize) {
  var LINKY_URL_REGEXP =
        /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,
      MAILTO_REGEXP = /^mailto:/;

  return function(text, target) {
    if (!text) return text;
    var match;
    var raw = text;
    var html = [];
    var url;
    var i;
    while ((match = raw.match(LINKY_URL_REGEXP))) {
      // We can not end in these as they are sometimes found at the end of the sentence
      url = match[0];
      // if we did not match ftp/http/mailto then assume mailto
      if (match[2] == match[3]) url = 'mailto:' + url;
      i = match.index;
      addText(raw.substr(0, i));
      addLink(url, match[0].replace(MAILTO_REGEXP, ''));
      raw = raw.substring(i + match[0].length);
    }
    addText(raw);
    return html.join('');

    function addText(text) {
      if (!text) {
        return;
      }
      html.push(text);
    }

    function addLink(url, text) {
      html.push('<a ');
      if (angular.isDefined(target)) {
        html.push('target="');
        html.push(target);
        html.push('" ');
      }
      html.push('href="');
      html.push(url);
      html.push('">');
      addText(text);
      html.push('</a>');
    }
  };
}]);

Comments

1

I wanted a pause button that swaps text. here is how I did it:

in CSS:

.playpause.paused .pause, .playpause .play { display:none; }
.playpause.paused .play { display:inline; }

in template:

<button class="playpause" ng-class="{paused:paused}" ng-click="paused = !paused">
  <span class="play">play</span><span class="pause">pause</span>
</button>

1 Comment

ngShow/ngHide do the same as what I am doing here, but with the bonus of ngAnimate working with it. ngIf is good for things where you actually want it removed from the DOM (audio/video/img elements, for example, with a single src.) docs.angularjs.org/api/ng/directive/ngIf
1

Inspired by @Neal I made this "no sanitize" filter from the newer Angular 1.5.8. It also recognizes addresses without ftp|http(s) but starting with www. This means that both https://google.com and www.google.com will be linkyfied.

angular.module('filter.parselinks',[])

.filter('parseLinks', ParseLinks);

function ParseLinks() {
  var LINKY_URL_REGEXP =
        /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
      MAILTO_REGEXP = /^mailto:/i;

  var isDefined = angular.isDefined;
  var isFunction = angular.isFunction;
  var isObject = angular.isObject;
  var isString = angular.isString;

  return function(text, target, attributes) {
    if (text == null || text === '') return text;
    if (typeof text !== 'string') return text;

    var attributesFn =
      isFunction(attributes) ? attributes :
      isObject(attributes) ? function getAttributesObject() {return attributes;} :
      function getEmptyAttributesObject() {return {};};

    var match;
    var raw = text;
    var html = [];
    var url;
    var i;
    while ((match = raw.match(LINKY_URL_REGEXP))) {
      // We can not end in these as they are sometimes found at the end of the sentence
      url = match[0];
      // if we did not match ftp/http/www/mailto then assume mailto
      if (!match[2] && !match[4]) {
        url = (match[3] ? 'http://' : 'mailto:') + url;
      }
      i = match.index;
      addText(raw.substr(0, i));
      addLink(url, match[0].replace(MAILTO_REGEXP, ''));
      raw = raw.substring(i + match[0].length);
    }
    addText(raw);
    return html.join('');

    function addText(text) {
      if (!text) {
        return;
      }
      html.push(text);
    }

    function addLink(url, text) {
      var key, linkAttributes = attributesFn(url);
      html.push('<a ');

      for (key in linkAttributes) {
        html.push(key + '="' + linkAttributes[key] + '" ');
      }

      if (isDefined(target) && !('target' in linkAttributes)) {
        html.push('target="',
                  target,
                  '" ');
      }
      html.push('href="',
                url.replace(/"/g, '&quot;'),
                '">');
      addText(text);
      html.push('</a>');
    }
  };
}

Comments

0

I would analyze the text in the link function on the directive:

directive("myDirective", function(){

  return {
        restrict: "A",
        link: function(scope, element, attrs){
          // use the 'element' to manipulate it's contents...
        }
      }
  });

3 Comments

I have tried this but when i use element.text('bla <a href="asd">asd</a>') it displays the html as text
could you share your jsfiddle
I have just updated the question as i realised i need more functionality

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.