Mihai Vilcu Mihai Vilcu - 4 months ago 15
AngularJS Question

using prismjs in angular app

I'm trying to use prismjs in my angular app

This is what i got so far

app.directive('nagPrism', [function() {
return {
restrict: 'A',
transclude: true,
scope: {
source: '='
},
link: function(scope, element, attrs, controller, transclude) {
if(!scope.source) {
transclude(function(clone) {
element.html(clone);
Prism.highlightElement(element.find("code")[0]);
});
} else {
scope.$watch(function() {return scope.source;}, function(v) {
if(v) {
Prism.highlightElement(element.find("code")[0]);
}
});
}

},
template: "<code ng-bind='source'></code>"
};

}]);


This will work if i got something like this

<!-- This works -->
<pre nag-prism source="code" class="language-css"></pre>


but i would like to make it work for something like this also

<pre nag-prism class="language-css">
<code>body {
color: red;
}
{{code}}

</code>
</pre>


For convenience i've made a plunkr

Any tips ?

Answer

As far as I understand, your target is to build directive that highlights code either given as a constant, dynamically by variable or mix of two above. If you are not attached to syntax from your post, here is solution.

Main problem is that before applying Prism highlight function, code template needs to be compiled in order to change {{variables}} to their values. In your solution highlight function is applied to raw template.

The idea is to change binding type from '=' to '@' and pass source code as an attribute in all cases.

Old binding:

scope: {
    source: '='
}

New binding:

scope: {
    source: '@'
}

Angular documentation:

@ or @attr - bind a local scope property to the value of DOM attribute. The result is always a string since DOM attributes are strings. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localName:'@myAttr' }, then widget scope property localName will reflect the interpolated value of hello {{name}}. As the name attribute changes so will the localName property on the widget scope. The name is read from the parent scope (not component scope).

Updated html sample (mind that old binding from first example also has changed!):

<!-- This works  -->
<pre nag-prism source="{{code}}" class="language-css"></pre>

<!-- Now this works either!  -->
<pre nag-prism source="body {
    color: red; 
}
{{code}}" class="language-css"></pre>

Full updated directive code:

app.directive('nagPrism', [function() {
    return {
        restrict: 'A',
        scope: {
            source: '@'
        },
        link: function(scope, element, attrs) {
            scope.$watch('source', function(v) {
                if(v) {
                    Prism.highlightElement(element.find("code")[0]);
                }
            });
        },
        template: "<code ng-bind='source'></code>"
    };
}]);

You can find working solution here: working sample

edit: directive updated to meet target from comment below

In order to properly display transcluded content, it must be manually compiled. It causes some changes in directive code (version below uses transcluded content if it is not empty and source is not defined):

app.directive('nagPrism', ['$compile', function($compile) {
    return {
        restrict: 'A',
        transclude: true,
        scope: {
          source: '@'
        },
        link: function(scope, element, attrs, controller, transclude) {
            scope.$watch('source', function(v) {
              element.find("code").html(v);

              Prism.highlightElement(element.find("code")[0]);
            });

            transclude(function(clone) {
              if (clone.html() !== undefined) {
                element.find("code").html(clone.html());
                $compile(element.contents())(scope.$parent);
              }
            });
        },
        template: "<code></code>"
    };
}]);

Following sample adds code block binded to input to show that linking works: improved sample

Comments