Site icon Tutorial

HTML Compiler

AngularJS’s HTML compiler allows the developer to teach the browser new HTML syntax. The compiler allows you to attach behavior to any HTML element or attribute and even create new HTML elements or attributes with custom behavior. AngularJS calls these behavior extensions directives.

HTML has a lot of constructs for formatting the HTML for static documents in a declarative fashion. For example if something needs to be centered, there is no need to provide instructions to the browser how the window size needs to be divided in half so that the center is found, and that this center needs to be aligned with the text’s center. Simply add an align=”center” attribute to any element to achieve the desired behavior. Such is the power of declarative language.

However, the declarative language is also limited, as it does not allow you to teach the browser new syntax. For example, there is no easy way to get the browser to align the text at 1/3 the position instead of 1/2. What is needed is a way to teach the browser new HTML syntax.

AngularJS comes pre-bundled with common directives which are useful for building any app. We also expect that you will create directives that are specific to your app. These extensions become a Domain Specific Language for building your application.

All of this compilation takes place in the web browser; no server side or pre-compilation step is involved.

Compiler

Compiler is an AngularJS service which traverses the DOM looking for attributes. The compilation process happens in two phases.

Some directives such as ng-repeat clone DOM elements once for each item in a collection. Having a compile and link phase improves performance since the cloned template only needs to be compiled once, and then linked once for each clone instance.

Directive

A directive is a behavior which should be triggered when specific HTML constructs are encountered during the compilation process. The directives can be placed in element names, attributes, class names, as well as comments. Here are some equivalent examples of invoking the ng-bind directive.

<span ng-bind=”exp”></span>

<span class=”ng-bind: exp;”></span>

<ng-bind></ng-bind>

<!– directive: ng-bind exp –>

A directive is just a function which executes when the compiler encounters it in the DOM. See directive API for in-depth documentation on how to write directives.

Here is a directive which makes any element draggable. Notice the draggable attribute on the <span> element.

imdex.html

<span draggable>Drag ME</span>

script.js

angular.module(‘drag’, []).

directive(‘draggable’, function($document) {

return function(scope, element, attr) {

var startX = 0, startY = 0, x = 0, y = 0;

element.css({

position: ‘relative’,

border: ‘1px solid red’,

backgroundColor: ‘lightgrey’,

cursor: ‘pointer’,

display: ‘block’,

width: ’65px’

});

element.on(‘mousedown’, function(event) {

// Prevent default dragging of selected content

event.preventDefault();

startX = event.screenX – x;

startY = event.screenY – y;

$document.on(‘mousemove’, mousemove);

$document.on(‘mouseup’, mouseup);

});

function mousemove(event) {

y = event.screenY – startY;

x = event.screenX – startX;

element.css({

top: y + ‘px’,

left:  x + ‘px’

});

}

function mouseup() {

$document.off(‘mousemove’, mousemove);

$document.off(‘mouseup’, mouseup);

}

};

});

The presence of the draggable attribute on any element gives the element new behavior. We extended the vocabulary of the browser in a way which is natural to anyone who is familiar with the principles of HTML.

How directives are compiled

It’s important to note that AngularJS operates on DOM nodes rather than strings. Usually, you don’t notice this restriction because when a page loads, the web browser parses HTML into the DOM automatically.

HTML compilation happens in three phases:

The result of this is a live binding between the scope and the DOM. So at this point, a change in a model on the compiled scope will be reflected in the DOM.

Below is the corresponding code using the $compile service. This should help give you an idea of what AngularJS does internally.

var $compile = …; // injected into your code

var scope = …;

var parent = …; // DOM element where the compiled template can be appended

var html = ‘<div ng-bind=”exp”></div>’;

// Step 1: parse HTML into DOM element

var template = angular.element(html);

// Step 2: compile the template

var linkFn = $compile(template);

// Step 3: link the compiled template with the scope.

var element = linkFn(scope);

// Step 4: Append to DOM (optional)

parent.appendChild(element);

The difference between Compile and Link

At this point you may wonder why the compile process has separate compile and link phases. The short answer is that compile and link separation is needed any time a change in a model causes a change in the structure of the DOM.

It’s rare for directives to have a compile function, since most directives are concerned with working with a specific DOM element instance rather than changing its overall structure.

Directives often have a link function. A link function allows the directive to register listeners to the specific cloned DOM element instance as well as to copy content into the DOM from the scope. Any operation which can be shared among the instance of directives should be moved to the compile function for performance reasons.

Understanding How Scopes Work with Transcluded Directives

One of the most common use cases for directives is to create reusable components. Below is a pseudo code showing how a simplified dialog component may work.

<div>

<button ng-click=”show=true”>show</button>

<dialog title=”Hello {{username}}.”

visible=”show”

on-cancel=”show = false”

on-ok=”show = false; doSomething()”>

Body goes here: {{username}} is {{title}}.

</dialog>

</div>

Clicking on the “show” button will open the dialog. The dialog will have a title, which is data bound to username, and it will also have a body which we would like to transclude into the dialog. Here is an example of what the template definition for the dialog widget may look like.

<div ng-show=”visible”>

<h3>{{title}}</h3>

<div class=”body” ng-transclude></div>

<div class=”footer”>

<button ng-click=”onOk()”>Save changes</button>

<button ng-click=”onCancel()”>Close</button>

</div>

</div>

This will not render properly, unless we do some scope magic. The first issue we have to solve is that the dialog box template expects title to be defined. But we would like the template’s scope property title to be the result of interpolating the <dialog> element’s title attribute (i.e. “Hello {{username}}”). Furthermore, the buttons expect the onOk and onCancel functions to be present in the scope. This limits the usefulness of the widget. To solve the mapping issue we use the scope to create local variables which the template expects as follows:

scope: {

title: ‘@’,             // the title uses the data-binding from the parent scope

onOk: ‘&’,              // create a delegate onOk function

onCancel: ‘&’,          // create a delegate onCancel function

visible: ‘=’            // set up visible to accept data-binding

}

Creating local properties on widget scope creates two problems:

To solve the issue of lack of isolation, the directive declares a new isolated scope. An isolated scope does not prototypically inherit from the parent scope, and therefore we don’t have to worry about accidentally clobbering any properties.

However isolated scope creates a new problem: if a transcluded DOM is a child of the widget isolated scope then it will not be able to bind to anything. For this reason the transcluded scope is a child of the original scope, before the widget created an isolated scope for its local variables. This makes the transcluded and widget isolated scope siblings.

This may seem to be unexpected complexity, but it gives the widget user and developer the least surprise. Therefore the final directive definition looks something like this:

transclude: true,

scope: {

title: ‘@’,             // the title uses the data-binding from the parent scope

onOk: ‘&’,              // create a delegate onOk function

onCancel: ‘&’,          // create a delegate onCancel function

visible: ‘=’            // set up visible to accept data-binding

},

restrict: ‘E’,

replace: true

Double Compilation, and how to avoid it

Double compilation occurs when an already compiled part of the DOM gets compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, and memory leaks. A common scenario where this happens is a directive that calls $compile in a directive link function on the directive element. In the following faulty example, a directive adds a mouseover behavior to a button with ngClick on it:

angular.module(‘app’).directive(‘addMouseover’, function($compile) {

return {

link: function(scope, element, attrs) {

var newEl = angular.element(‘<span ng-show=”showHint”> My Hint</span>’);

element.on(‘mouseenter mouseleave’, function() {

scope.$apply(‘showHint = !showHint’);

});

attrs.$set(‘addMouseover’, null); // To stop infinite compile loop

element.append(newEl);

$compile(element)(scope); // Double compilation

}

}

})

At first glance, it looks like removing the original addMouseover attribute is all there is needed to make this example work. However, if the directive element or its children have other directives attached, they will be compiled and linked again, because the compiler doesn’t keep track of which directives have been assigned to which elements.

This can cause unpredictable behavior, e.g. ngClick or other event handlers will be attached again. It can also degrade performance, as watchers for text interpolation are added twice to the scope.

Double compilation should therefore be avoided. In the above example, only the new element should be compiled:

angular.module(‘app’).directive(‘addMouseover’, function($compile) {

return {

link: function(scope, element, attrs) {

var newEl = angular.element(‘<span ng-show=”showHint”> My Hint</span>’);

element.on(‘mouseenter mouseleave’, function() {

scope.$apply(‘showHint = !showHint’);

});

element.append(newEl);

$compile(newEl)(scope); // Only compile the new element

}

}

})

Another scenario is adding a directive programmatically to a compiled element and then executing compile again. See the following faulty example:

<input ng-model=”$ctrl.value” add-options>

angular.module(‘app’).directive(‘addOptions’, function($compile) {

return {

link: function(scope, element, attrs) {

attrs.$set(‘addOptions’, null) // To stop infinite compile loop

attrs.$set(‘ngModelOptions’, ‘{debounce: 1000}’);

$compile(element)(scope); // Double compilation

}

}

});

In that case, it is necessary to intercept the initial compilation of the element:

angular.module(‘app’).directive(‘addOptions’, function($compile) {

return {

priority: 100, // ngModel has priority 1

terminal: true,

compile: function(templateElement, templateAttributes) {

templateAttributes.$set(‘ngModelOptions’, ‘{debounce: 1000}’);

// The third argument is the max priority. Only directives with priority < 100 will be compiled,

// therefore we don’t need to remove the attribute

var compiled = $compile(templateElement, null, 100);

return function linkFn(scope) {

compiled(scope) // Link compiled element to scope

}

}

}

});

Exit mobile version