angularjs, $compiled templates and ng-repeat - angularjs

Essentially, I take two templates inside a compile function of a directive.
I compile them once and for all (via $compile) in the compile phase of the directive.
In the link phase of the directive, I watch a scope variable and applies one or the other compiled template to the scope.
testApp.directive('testField', ['$compile', function ($compile) {
return {
restrict: 'E',
scope: true,
compile: function(tElement, tAttrs) {
var viewFn = $compile("<div>View: <span ng-repeat='x in [1,2,3]'>{{x}}</span></div>");
var editFn = $compile("<div>Edit: <span ng-repeat='x in [4,5,6]'>{{x}}</span></div>");
return function (scope, element, attrs) {
var innerScope = null;
scope.$watch("mode", function (mode) {
if (innerScope) innerScope.$destroy();
innerScope = scope.$new();
if (mode == 'VIEW') {
element.empty().append(viewFn(innerScope));
} else {
element.empty().append(editFn(innerScope));
}
});
};
}
};
}]);
It works fine, except when the template includes a Ng-repeat that is not the root element in which case it behave strangely:
To reproduce, go to http://plnkr.co/edit/RCqlNWTVdXVoMQCkFcQn?p=preview
And switch a couple of time from Edit to View.
You'll notice the number of iterations of ng-repeat grows over time.
First time, it displays 123 and 456, like it should
After the first back and forth between view and edit, it displays 123123 and 456456
And it keeps adding one iteration every time you do a back and forth between view and edit.

I probably found the solution shortly after posting.
The problem apparently lies in the fact that ng-repeat needs to clone the template.
After stumbling on a comment in the angular source, turns out we can pass a second argument to the function returned by compile.
This second argument is a function that is called with a clone of the template (and the scope being linked to).
so instead of
if (mode == 'VIEW') {
element.empty().append(viewFn(innerScope));
} else {
element.empty().append(editFn(innerScope));
}
I can do
function setElts(elts) {
element.empty().append(elts);
}
if (mode == 'VIEW') {
viewFn(innerScope, setElts);
} else {
editFn(innerScope, setElts);
}

Maybe you can do like this.
Firstly, you inject new dom directly in your view. After, catch it and apply $compile.
like this;
var viewFn = "<div>View: <span ng-repeat='x in [1,2,3]'>{{x}}</span></div>";
...
element.html(viewFn);
...
$compile(tElement.contents())(innerScope);

Related

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

How to dynamically insert and remove one nested Angular custom directives?

I'm relatively new to creating custom Angular directives, and I'm trying to replace a page in my application which is a series of JQuery components which get nested inside one another.
I'm wanting to create a set of custom Angular directives, which I can nest within each other allowing the user to build up a kind of form, whilst allowing them to delete and re-add any directives nested within one another if they need to.
I'm not sure how dynamically insert one directive into another, based on a user's choice, and how to set this within the directive so that a given directive's child can be deleted, re-added and then recompiled.
So far the only (unsophisticated) method I have come up with is to 2-way bind to an attribute on the directive's isolate scope, and have this determine the inner content of the directive like so, however upon changing this attribute object on the parent scope, the directive's DOM doesn't recompile, so I'm convinced there is a much better way to do this.
I'm assuming I might need to use transclusion or the link function in some way, so any guidance on this is much appreciated!
Current attempt:
app.directive("testCustomMapperThings", function($compile) {
var testTemplate1 = '<div>THIS IS FIRST THE DYNAMICALLY COMPILED TEST TEMPLATE 1</div>';
var testTemplate2 = '<div>THIS IS SECOND THE DYNAMICALLY COMPILED TEST TEMPLATE 2</div>';
var getTemplate = function(contentType) {
var template = '';
switch(contentType) {
case 'testTemplate1':
template = testTemplate1;
break;
case 'testTemplate2':
template = testTemplate2;
break;
}
return template;
};
var linker = function(scope, element, attrs) {
//reads the scope's content attribute (2 way bound to this directive's isolate scope) and sets as DOM
element.html(getTemplate(scope.content.testContent)).show();
//compiles the 2 way bound DOM, recompiles directive on changes to attributes. todo: CHECK DIRECTIVE RECOMPILES ON CHANGES TO ATTRIBUTES
$compile(element.contents())(scope);
};
return {
restrict: "E", //can only be an element
link: linker, //link function
scope: { //isolate scope, 2 way bind to a 'content' attribute
content:'='
}
};
});
Use of this directive in the DOM, where I attempted to alter the $scope.content object but the directive's inner content didn't recompile:
<test-custom-mapper-things content="testContent"></test-custom-mapper-things>
<button ng-click="changeContent()">Click to change the test content and see if DOM recompiles</button>
Controller for the parent scope of the directive:
$scope.testContent = {
testContent : "testTemplate1"
};
$scope.changeContent = function() {
if($scope.testContent.testContent == 'testTemplate1') {
$scope.testContent.testContent = 'testTemplate2';
} else {
$scope.testContent.testContent = 'testTemplate1';
}
};
Use the $compile service and scope.$watch method.
Example:
Javascript:
angular.module('dynamicTemplate',[])
.directive("testCustomMapperThings",['$compile',function($compile) {
return {
resctrict: 'AE',
scope: {
active: '='
},
link: function (scope, el, attrs, ctrl) {
var t1 = "<div>I'm 1st Template</div>",
t2 = "<div>I'm 2nd Template</div>",
newTemplate;
function loadTemplate() {
el.html('');
switch(scope.active) {
case 'template1':
newTemplate = t1;
break;
case 'template2':
newTemplate = t2;
break;
}
el.append($compile(newTemplate)(scope));
}
loadTemplate();
scope.$watch('active', function(newVal, oldVal){
if(newVal === oldVal) return;
loadTemplate();
});
}
}
}])
.controller('templateController', function() {
var vm = this;
vm.option = true;
vm.templateOption = vm.option? 'template1' : 'template2';
vm.change = function() {
vm.option = !vm.option;
vm.templateOption = vm.option? 'template1' : 'template2';
}
});
HTML
<div ng-app="dynamicTemplate">
<div ng-controller="templateController as t">
<button ng-click="t.change()"></button>
<test-custom-mapper-things active="t.templateOption"></test-custom-mapper-things>
</div>
</div>
Codepen: http://codepen.io/gpincheiraa/pen/vLOXGz

How can I be notified when DOM elements are added to my directive?

I've got a simple directive that draws a few elements, like in this example. I want to programatically set some style properties but in the link function, the elements are apparently not there yet.
Here's a fiddle.
What I think is happening is that when I call the colorSquares function, there are no squares yet in the DOM. Wrapping it in a $timeout, it works, but that just feels so wrong.
Is there any way I can be notified when the elements exist? Or is there a place that I can put the code which will access them that is guaranteed to run after they exist?
myApp.directive('myDirective', ['$timeout', function ($timeout) {
return {
restrict: 'E',
replace: false,
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
function colorSquares() {
var squaresFromDOM = document.getElementsByClassName('square');
for (var i = 0; i < squaresFromDOM.length; i++) {
squaresFromDOM[i].style['background-color'] = '#44DD44';
}
}
// this does not work, apparently because the squares are not in the DOM yet
colorSquares();
// this works (usually). It always works if I give it a delay that is long enough.
//$timeout(colorSquares);
},
template: '<div><div ng-repeat="s in squares" class="square"></div></div>'
};
}]);
You should work with Angular rather than against it which is to say you should use data bindings to do what you are trying to do rather than events/notifications in this context.
http://jsfiddle.net/efdwob3v/5/
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
scope.style = {"background-color": "red"};
},
template: '<div><div ng-repeat="s in squares" class="square" ng-style="style"></div></div>'
That said there's no difference in doing the above and just using a different class that has that red background color or even just doing style="background-color: red;"
you put the answer in your qeustion, "It always works if I give it a delay that is long enough.".
So just make the delay long enough, in this situation that can be achieved by adding an onload event because when the elements get added to the DOM it calls that event.
So instead of just colorSquares(); you could use:
window.addEventListener("load", colorSquares);
Though this may not be the ideal solution since it will also trigger when something else triggers the onload event.
Answering your question directly. To know if an element is added to a directive or to the DOM in general, you can simply put a directive on that element, since the directive will run only when the element on which it "sits" is already in the DOM.
Using part of your code as an example:
myApp.directive('myDirective', function () {
return {
...
//put custom directive that will notify when DOM is ready
template: '<div><div ng-repeat-ready ng-repeat="s in squares" class="square"></div></div>'
};
});
And here is the custom ng-repeat-ready directive:
myApp.directive('ngRepeatReady', function () {
return {
link: function (scope) {
if (scope.$last) {
//do notification stuff here
//for example $emit an event
scope.$emit('ng-repeat is ready');
}
}
}
});
This directive will run when the element on which is sits is already in the DOM and check if the element has $last property on the scope (ng-repeat sets this flag for the last element of the iterated object) which means that the ng-repeat directive is done and you can now operate on the DOM safely.

angularjs conditional directive only on desktop

I want to put a directive on an element only if it's not viewed from mobile. As it's an externally maintained plugin I don't want to modify the directive itself. What's the easiest way of doing this?
Create a directive that performs the detection (or receives it), adds the directive if not viewed from mobile, removes itself from the element and then compiles the element.
HTML:
<div not-on-mobile="external-directive">Hello.</div>
JS:
app.directive('notOnMobile', function($compile) {
// Perform detection.
// This code will only run once for the entire application (if directive is present at least once).
// Can be moved into the compile function if detection result needs to be passed as attribute.
var onMobile = false;
return {
compile: function compile(tElement, tAttrs) {
if (!onMobile) tElement.attr(tAttrs.notOnMobile, '');
tElement.removeAttr('not-on-mobile');
return function postLink(scope, element) {
$compile(element)(scope);
};
}
};
});
Demo: http://plnkr.co/edit/nJntmfiLZ20JCdSWiRDQ?p=preview

angular ng-bind-html and directive within it

Plunker Link
I have a element which I would like to bind html to it.
<div ng-bind-html="details" upper></div>
That works. Now, along with it I also have a directive which is bound to the bound html:
$scope.details = 'Success! <a href="#/details/12" upper>details</a>'
But the directive upper with the div and anchor do not evaluate. How do I make it work?
I was also facing this problem and after hours searching the internet I read #Chandermani's comment, which proved to be the solution.
You need to call a 'compile' directive with this pattern:
HTML:
<div compile="details"></div>
JS:
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}])
You can see a working fiddle of it here
Thanks for the great answer vkammerer. One optimization I would recommend is un-watching after the compilation runs once. The $eval within the watch expression could have performance implications.
angular.module('vkApp')
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
}
);
};
}]);
Here's a forked and updated fiddle.
Add this directive angular-bind-html-compile
.directive('bindHtmlCompile', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
// Incase value is a TrustedValueHolderType, sometimes it
// needs to be explicitly called into a string in order to
// get the HTML string.
element.html(value && value.toString());
// If scope is provided use it, otherwise use parent scope
var compileScope = scope;
if (attrs.bindHtmlScope) {
compileScope = scope.$eval(attrs.bindHtmlScope);
}
$compile(element.contents())(compileScope);
});
}
};
}]);
Use it like this :
<div bind-html-compile="data.content"></div>
Really easy :)
Unfortunately I don't have enough reputation to comment.
I could not get this to work for ages. I modified my ng-bind-html code to use this custom directive, but I failed to remove the $scope.html = $sce.trustAsHtml($scope.html) that was required for ng-bind-html to work. As soon as I removed this, the compile function started to work.
For anyone dealing with content that has already been run through $sce.trustAsHtml here is what I had to do differently
function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(function(scope) {
return $sce.parseAsHtml(attrs.compile)(scope);
},
function(value) {
// when the parsed expression changes assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current scope.
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
});
}
This is only the link portion of the directive as I'm using a different layout. You will need to inject the $sce service as well as $compile.
Best solution what I've found! I copied it and it work's exactly as I needed. Thanks, thanks, thanks ...
in directive link function I have
app.directive('element',function($compile){
.
.
var addXml = function(){
var el = $compile('<xml-definitions definitions="definitions" />')($scope);
$scope.renderingElement = el.html();
}
.
.
and in directive template:
<span compile="renderingElement"></span>

Resources