Angular.js

Web-components like AngularJS directives

As you may already know web components consist out of a set of technologies which are combined to create a custom element for use in your HTML markup. The main additions, as described in several blogposts, are HTML imports, Shadow Dom and Templates combined with isolated scripts and styling. (If these concepts are new to you i suggest you read up on web components at WebComponents.org).

This blog post has a living example on plnkr.co.

If we look at Angular it already supports html imports and isolated scripts through it’s directive approach. This means we can already create custom components by using directives. The downside of this approach however is that there is no true isolation of markup and styling. Meaning both markup and styling may be inadvertently influenced by an outside source.

Let’s start with a basic directive and template:

angular.module('shadow.app', ['component.api'])
.directive('simpleDirective', function() {
    return {
      restrict: 'E',
      replace: false,
      templateUrl: 'template.html',
      transclude: true,
      scope: {
        dynamic: '='
      },
      link: function($scope, element) {
        // your code here
      }
    };
  })
<div class="outer">
  <div class="boilerplate">
    This is template content
  </div>
  <div class="name">
    <div ng-transclude></div>
  </div>
  <div class="boilerplate">
    {{dynamic}}
  </div>
</div> 

As you can see lines 6 and 11 in the directive have been highlighted. This is because these lines will be where we’ll add our logic to turn this normal directive in to the essence of a true web component.

Now the first thing we’ll want to do is make sure that the template we get is correct. In order to do this we will be creating a function which retrieves the template and encapsulates it inside a template. You could also do this in the template it’s self, but it’ll soon become clear why I choose to work this way.

The method for doing this will require $interpolate and $templateCache to be added to your directive. As we’ll be getting the template from the cache directly and create a template stamp. It should look something like this.

var shadowTemplate = $interpolate('<div ng-transclude></div>' +
    '<template>{{template}}</template>'); 
function handleShadowTemplate(url) {
  var template = $templateCache.get(url);

  template =
    template.replace('<div ng-transclude></div>', '<content></content>');
  template = shadowTemplate({
    template: template
  })

  return template;
} 

This effectively wraps the markup inside a Template for later use with the shadow root whilst still maintaining a link inside it with our transcluded content through the content tag. The trick here is to create a wrapper for the link function. You could also use the link function for this but it’ll become clear later on why i choose not follow said approach.

The function would look something like this:

function shadowLink(linkCallback) {
  return function($scope, element, attr, controllers, transcludeFn) {
      var shadow = element.find('div')[0].createShadowRoot(),
          template = element.find('template')[0],
          clone = document.importNode(template.content, true);

    shadow.appendChild(clone);
    $compile(shadow)($scope);

    linkCallback($scope, element, attr, controllers, transcludeFn);
  }
} 

This effectively retrieves the transcluding tag which will now also become our shadow root, retrieve our template (the one we stamped out earlier) and clone it’s content and now the magic begins. Well not really magic but something interesting nonetheless. We add our cloned content to our shadow root and then compile it with the scope of our directive.

This effectively binds our scope content to our shadow-document. Allowing data binding to function as it normally would in a AngularJS directive. Only now our markup is isolated and will not be as prone to accidental manipulation.

Now I mentioned styling earlier on. And in many blogs you’ll see people add styling inline. I don’t really like that method so I personally prefer to use the @import statement but that is mainly preference I think.

And as for the reason for the wrappers. Using methods en encapsulate our callbacks it’s possible to move them into a service allowing us to use these exact same methods in multiple directives whilst maintaining a single point of maintainability. Like so:

angular.module('component.api', [])
  .provider('ShadowService', function() {
    this.$get = ['$compile', '$interpolate', '$templateCache',

      function($compile, $interpolate, $templateCache) {
        var shadowTemplate = $interpolate('<div ng-transclude></div>' +
          '<template>{{template}}</template>');

        function handleShadowTemplate(url) {
          // code here
        }

        function shadowLink(linkCallback) {
          // Code here
        }

        return {
          shadowTemplate: handleShadowTemplate,
          shadowLink: shadowLink
        };
      }
    ];
  });

Always good to keep up with the DRY principle and all. You can see more on how I implemented the provider in my plunker example. And also how I prevent shadow dom execution when createShadowRoot is not supported.

If you want to see more or just play with this setup see the plunker i’ve created for your enjoyment. I hope you’ve enjoyed reading this and please ask any questions you may have through the comments section below.

Reference: Web-components like AngularJS directives from our WCG partner Richard Rijnberk at the JDriven blog.

Richard Rijnberk

Richard is an experienced Java software engineer and Frontend Developer. Over the last period he has mainly focused on Frontend development with frameworks such as Angular. He has an interest in gadgets and is always eager to learn new technologies. He currently works for JDriven and is one of the bloggers on their site.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button