AngularJS1 directive with `terminal:true` will disable rendering of expressions, why? - angularjs

HTML code:
<div ng-controller="MyController">
<div>{{ hello }}</div>
<div my-terminal>{{hello}}</div>
</div>
JS code:
const app = angular.module('app', [])
app.controller('MyController', function ($scope) {
$scope.hello = 'Hello, AngularJS'
})
app.directive('myTerminal', function () {
return {
restrict: 'A',
terminal: true,
link: function () {
console.log('--- myTerminal')
}
}
})
Please notice the terminal is true.
Result:
From angularjs document, I found when terminal is true, any other directives applied on the same element with lower priority will not be executed, but I can't explain why <div my-terminal>{{hello}}</div> will not render the expression {{hello}}
A small complete demo for this question: https://github.com/freewind-demos/angularjs1-directive-terminal-issue-demo

https://github.com/angular/angular.js/blob/master/src/ng/compile.js :
function addTextInterpolateDirective(directives, text) {
var interpolateFn = $interpolate(text, true);
if (interpolateFn) {
directives.push({
priority: 0,
compile: function textInterpolateCompileFn(templateNode) {
var templateNodeParent = templateNode.parent(),
hasCompileParent = !!templateNodeParent.length;
...
So using expression {{}} results in adding directive. Guess thats why it is affected by 'terminate' property.

From the Docs:
terminal
If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined). Note that expressions and other directives used in the directive's template will also be excluded from execution.
— AngularJS Comprehensive Directive API Reference - terminal
A better explaination of what that means comes from the Docs for ng-non-bindable which uses the terminal property:
ngNonBindable
The ngNonBindable directive tells AngularJS not to compile or bind the contents of the current DOM element, including directives on the element itself that have a lower priority than ngNonBindable. This is useful if the element contains what appears to be AngularJS directives and bindings but which should be ignored by AngularJS. This could be the case if you have a site that displays snippets of code, for instance.
— AngularJS ng-non-bindable Directive API Reference

You need to use ng-bind
<div ng-controller="MyController">
<div>{{hello}}</div>
<div my-terminal ng-bind="hello"></div>
</div>
The DEMO
const app = angular.module('app', [])
app.controller('MyController', function ($scope) {
$scope.hello = 'Hello, AngularJS'
})
app.directive('myTerminal', function () {
return {
restrict: 'A',
terminal: true,
link: function () {
console.log('--- myTerminal')
}
}
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app" ng-controller="MyController">
<div>{{ hello }}</div>
<div my-terminal ng-bind="hello"></div>
</body>

Related

AngularJS: Can't call onClick() when using ng-bind-html [duplicate]

I've included a Plunker here: http://plnkr.co/edit/4vqV8toHo0vNjtfICtzI?p=preview
I'm trying to add a button to the DOM and when clicked should execute the function bound to it. In this case it should alert "testing". Here is the code.
controller
app.controller('MainCtrl', function($scope, $sce) {
$scope.trustedHtml = $sce.trustAsHtml('<button ng-click="testAlert()">Submit</button>');
$scope.testAlert = function () {
alert('testing')
};
});
HTML
<body ng-controller="MainCtrl">
<div ng-bind-html="trustedHtml"></div>
</body>
$sce.trustAsHtml and ng-bind-html are not meant to build HTML with directives. This technique will not work.
This is because angular works by first compiling and then linking. See the conceptual overview for a good explaination.
In short, by the time you link the HTML defined in your trustAsHtml, it is too late for angular to compile (and therefore understand) the ng-click directive.
In order to dynamically add HTML, you should be looking at the $compile service (and/or directives). Docs are here.
For Angular 1.6.1, I found a solution that worked for me.
template:
<div ng-bind-html="trustAsHtml(content);" init-bind> </div>
In controller:
$scope.trustAsHtml = function(string) {
return $sce.trustAsHtml(string);
};
Directive:
.directive('initBind', function($compile) {
return {
restrict: 'A',
link : function (scope, element, attr) {
attr.$observe('ngBindHtml',function(){
if(attr.ngBindHtml){
$compile(element[0].children)(scope);
}
})
}
};
})

Angular 2 way binding doesn't work with template even though I'm not using primatives

I can't figure out what's wrong with my code. I'm following the example of this fiddle in order to bind the two inputs. What I'm trying to do is just a little more involved- I'd like to load a certain template based on an attribute passed into the directive. I can't figure out what's wrong over here.
This is my html
<!-- template -->
<script type="text/ng-template" id="X-template.html">
<button ng-click='clickMe()'><b>Check model</b></button>
Directive: <input ng-model="this.test"></input>
</script>
<div ng-controller="MyCtrl">
<my-directive type="X"></my-directive>
No Directive:<input ng-model="this.test"></input>
{{this.test}}
</div>
And my JS:
var app = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.this= {test : "asdf"};
$scope.clickMe = function() {
alert($scope.this.test);
}
}
app.directive('myDirective', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
element.append('<div ng-include="\'' + attrs.type + '-template.html\'"></div>');
}
}
});
The issue is with trying to use this, which is a reserved keyword in JavaScript, as a property name.
Try using a different property name. I changed your example to use foo in the link below.
http://jsfiddle.net/01h3ne4y/

How can I use ng-repeat to generate custom html tags or directive?

I am trying to create some custom tag using angularjs with ng-repeat but its not working, its giving tag name in double quotation marks.
Good result is: shouldn't show <div> or another tags.
I have a demo version here : http://plnkr.co/edit/aT2UjMIGGLvnCGFxXC3a?p=preview
or you can use code snippet
my code is:
angular.module('myApp', [])
.controller('MainCtrl', function($scope) {
$scope.items = [
{
"name":"directive1"
},
{
"name":"directive2"
},
{
"name":"div"
}
];
})
.directive("showitems", function(){
return {
restrict: "E",
template: '<div class="item" ng-repeat="item in items"><div class="item-title">{{item.name}}</div><div class="item-body"><{{item.name}}></{{item.name}}></div></div>'
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MainCtrl">
<showitems></showitems>
</div>
AFAIK, doing something like <{{item.name}}></{{item.name}}> will never work in angular since the markups aren't treated as new DOM elements. A much better, and more manageable approach is to write directives for all possible types you'd want to render as DOM elements.
Also, to know more about DOM manipulations from within a directive, read up about $compile: here
An alternate way of doing this would be something like this in your directive template:
<directive1 ng-if="directiveType == 'directive1'"></directive1>
<directive2 ng-if="directiveType == 'directive1'"></directive2>
<directive3 ng-if="directiveType == 'directive1'"></directive3>
And in your controller/directive, you must declare directiveType to the type of directive you want to render.
Instead of wrapping {{item.name}} into its own tag, make a customTag directive. This can take a string (the tag name) and create and compile an element with it.
.directive("customTag", function ($compile) {
return {
restrict: "A",
link: function (scope, element, attrs) {
//pass customTag in as part of the directive attribute
var customTag = attrs.customTag;
if (!customTag) return;
var customElem = angular.element("<" + customTag + "></" + customTag + ">");
//replace your custom element directive with the compiled one
element.replaceWith($compile(customElem)(scope));
}
}
}
Then in your template.html:
<div class="item" ng-repeat="item in items">
<div class="item-title">{{item.name}}</div>
<div class="item-body">
<!-- This will use customTag directive to create element item.name -->
<div data-custom-tag="{{item.name}}"></div>
</div>
</div>
A div tag (or any non-directive) wouldn't be too useful because it doesn't get any innerHTML or behavior like directive1 and directive2 would get. This of course is a simple example, you may want to put some more checks or restrictions in there, but this should get you started.
See fiddle here that builds on your example.

Template manipulation in Angular.js

I am trying to modify an Angular template before any other directive is triggered, in particular interpolation. I am doing this through the compile option in the directive definition.
Here is my test code:
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.js"></script>
<script>
angular.module('soQuestion', [])
.directive('example', function () {
return {
compile: function (tElement, tAttrs) {
if (tAttrs.foo && tAttrs.foo.match(/^keyword/)) {
tElement.attr('foo', 'prefix-' + tAttrs.foo);
console.log(tElement.attr('foo'));
}
}
};
})
.controller('controller', function($scope) {
$scope.value = 'something';
});
</script>
</head>
<body ng-app="soQuestion">
<div ng-controller="controller">
<div example foo="keyword_{{value}}"></div>
</div>
</body>
</html>
However, the final result that I get is <div foo="keyword_something"></div> instead of <div foo="prefix-keyword_something"></div>, even if the compile function was triggered properly. What is going on here?
Its a directive priority issue, and admittedly I still don't completely understand. But don't for get that {{}} is itself a directive. Its getting applied in some order with yours, and overwriting your manipulation. If its terminal and high priority, it clears up.
DEMO
angular.module('soQuestion', [])
.directive('example', function () {
return {
priority: 1000,
terminal : true,
compile: function (tElement, tAttrs) {
if (tAttrs.foo && tAttrs.foo.match(/^keyword/)) {
tElement.attr('foo', 'prefix-' + tAttrs.foo);
console.log(tElement.attr('foo'), tElement[0]);
}
return function(){};
}
};
})
.controller('controller', function($scope) {
$scope.value = 'something';
});
because this breaks the {{}} I would consider compiling the attr manually as well.
angular.module('soQuestion', [])
.directive('example', function () {
return {
compile: function (tElement, tAttrs) {
if (tAttrs.foo && tAttrs.foo.match(/^keyword/)) {
// Change the tAttrs hash instead of the element itself
tAttrs.foo = 'prefix-' + tAttrs.foo;
// Change the element too, in case no interpolation is present
tElement.attr('foo', tAttrs.foo);
console.log(tElement.attr('foo'));
}
}
};
})
Explanation: the interpolate directive does check for changes in the attribute value. However it doesn't look again on the DOM node itself but on the tAttrs hash.
Old pessimistic answer:
I don't think it's possible to achieve the desired result.
Looking at Angular's source, collectDirectives puts together the list of directives that affect a certain node. Their respective compile functions are collected and then sorted by priority. The problem is that the compile function of {{}} is then bound to keyword_{{value}} and is not affected by the example directive.

ng-repeat in combination with custom directive

I'm having an issue with using the ng-repeat directive in combination with my own custom directive.
HTML:
<div ng-app="myApp">
<x-template-field x-ng-repeat="field in ['title', 'body']" />
</div>
JS:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
compile: function(element, attrs, transcludeFn) {
element.replaceWith('<input type="text" />');
}
};
});
See jSFiddle
The problem here is that nothing is replaced. What I'm trying to accomplish is an output of 2x input fields, with the 'x-template-field' tags completely replaced in the DOM. My suspicion is that since ng-repeat is modifying the DOM at the same time, this won't work.
According to this Stack Overflow question, the accepted answer seems to indicate this has actually worked in earlier versions of AngularJS (?).
Wouldn't element.html('...') work?
While element.html('...') actually injects the generated HTML into the target element, I do not want the HTML as a child element of the template tag, but rather replace it completely in the DOM.
Why don't I wrap my template tag with another tag that has the ng-repeat directive?
Basically, for the same reason as above, I don't want my generated HTML as a child element to the repeating tag. While it would probably work decently in my application, I would still feel like I've adapted my markup to fit Angular and not the other way around.
Why am I not using the 'template' property?
I haven't found any way to alter the HTML retrieved from the 'template' / 'templateUrl' properties. The HTML I want to inject is not static, it's dynamically generated from external data.
Am I too picky with my markup?
Probably. :-)
Any help is appreciated.
Your directive needs to run before ng-repeat by using a higher priority, so when ng-repeat clones the element it is able to pick your modifications.
The section "Reasons behind the compile/link separation" from the Directives user guide have an explanation on how ng-repeat works.
The current ng-repeat priority is 1000, so anything higher than this should do it.
So your code would be:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
priority: 1001, <-- PRIORITY
compile: function(element, attrs, transcludeFn) {
element.replaceWith('<input type="text" />');
}
};
});
Put your ng-repeat in the template. You could modify attributes of element and accordingly in directive to determine if ng-repeat is needed, or what data to use inside the directive compiling
HTML(attribute):
<div ng-app="myApp" template-field></div>
JS:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'A',
template:'<input type="text" value="{{field}" ng-repeat="field in [\'title\',\'body\']" />'
};
});
DEMO: http://jsfiddle.net/GDfxd/3/
Also works as an element :
HTML(element):
<div ng-app="myApp" >
<template-field/>
</div>
JS
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
replace:true,
template:'<input type="text" value="{{field}}" ng-repeat="field in [\'title\',\'body\']" />'
};
});
DEMO: http://jsfiddle.net/GDfxd/3/

Resources