Angular save watch expression from attr in directive - javascript

I want to pass some html to attr in directive, the problem i have that what i do does not save binding it is only display the initial value, how can i accomplish data binding here?
function link( $scope, elem, attrs, ctrl, transclude ) {
$scope.dropDownClass = attrs.ddClass || 'default';
var main = $compile(attrs.main)($scope);
elem.find('button').first().append(main);
}
<my-drop-down main='<a><img src="images/flags/en.png" alt="en"/>{{name}}</a>'>
<li><a><img src="images/flags/en.png" alt="en"/>En</a></li>
</my-drop-down>
I want the {{name}} to still be binding to my controller.
http://plnkr.co/edit/IrF1dIZslCEQf0FOfNJi?p=preview

Okay, I took a quick look. From what I could see, the main attribute is being evaluated before your attr lookup. So, your actually compiling <a>World</a> not <a>{{name}}</a>.
I don't think there is a way to tell angular not to evaluate an attribute (there is ngNonBindable for markup, but that doesn't help us). I see two solutions for your problem.
Option #1: You could pull out your desired template string from an attribute and, instead, attach it to your scope in MainCtrl. That looks like this: http://plnkr.co/edit/M6GZJDVHuW8op2Zo11mJ?p=preview
// Markup:
<my-drop-down> ...
// MainCtrl:
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.main = '<a>{{name}}</a>';
});
// Then, in your directive link:
function link($scope, elem, attrs, ctrl, transclude ) {
$scope.dropDownClass = attrs.ddClass || 'default';
// Use scope.main, instead of attrs.main:
var main = $compile($scope.main)($scope);
elem.find('button').first().append(main);
}
Option #2: If you wanted to keep the passing of the template string in an attribute we cannot, as far as I can tell, use curly braces and expect them to be passed through as curly braces. So, we could try to use something else that is unique enough for our code. I chose %%, but it could be anything you wanted, really. That would look like this: http://plnkr.co/edit/760CFsq9sF9lIBHgO2Ic?p=preview
// Markup:
<my-drop-down main="<a>%%name%%</a>">
// Then, in your directive link:
function link($scope, elem, attrs, ctrl, transclude ) {
$scope.dropDownClass = attrs.ddClass || 'default';
// Replace our template string and compile that w/ braces:
var tpl = attrs.main.replace(/%%([a-z]+)%%/g, '{{$1}}');
var main = $compile(tpl)($scope);
elem.find('button').first().append(main);
}
There are other options you could take, like creating an isolate scope, etc. but they require more refactoring of your code. The above two seem to be the easiest to drop-in.

Problem is that when your link function is executed, name is already interpolated.
In addition to what #rgthree said you could also use the compile function on directive to only call var compiled = $compile(tAttrs.main) and then in the link function call the return value with the scope var main = compiled($scope):
compile: function compile(tElement, tAttrs, transclude) {
var compiled = $compile(tAttrs.main);
return function( $scope, elem, attrs, ctrl, transclude ) {
$scope.dropDownClass = attrs.ddClass || 'default';
console.log(3);
var main = compiled($scope);
elem.find('button').first().append(main);
}
},
Check this plunker

Related

How The Object Scope Get That update available and get it ready to be exposed on the view AngularJs

In AngularJS the data-binding work to expose immediate data to our View !!
this stuff's due of the object scope which is the glue between the Logic Code AND The View.
Also all we know that AngularJs support the tow-way-binding !!!
My Question Is :
How the $scope can know that there object binding was changed or not??
if there while condition inside scope for auto-change detect or what?
Check angular.js file, we will get the code for ngBindDirective:
var ngBindDirective = ['$compile', function($compile) {
return {
restrict: 'AC',
compile: function ngBindCompile(templateElement) {
$compile.$$addBindingClass(templateElement);
return function ngBindLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.ngBind);
element = element[0];
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
element.textContent = isUndefined(value) ? '' : value;
});
};
}
};
}];
Note the last two line, it used watcher for attribute ngBind, for any change it apply to the element.

How to avoid using 'scope.$parent...' in angular 1.2

We're developing a set of (ideally) flexible, component-based re-usable templates in angularjs 1.2 to develop a series of e-learning modules.
Part of the spec requires the tracking of 'completable' components. At the moment the main controller looks like this:
app.controller('mainCtrl', ['$scope', function($scope) {
$scope.completables = [];
$scope.completed = [];
$scope.addCompletable = function (object) {
$scope.completables.push(object);
// also set correlating completed property to 'false' for each completable added
$scope.completed.push(false);
}
$scope.componentCompleted = function(id) {
// Set complete to 'true' for matching Sscope.completed array index
// We COULD use .indexOf on the completables array, but that doesn't work with IE8
var tempArray = $scope.completables;
var matchingIndex = -1;
for (var i=0; i<tempArray.length; i++) {
if (tempArray[i]==id) {
matchingIndex = i;
}
}
if (i>-1) {
$scope.completed[matchingIndex] = true;
}
}
}]);
We have a eng-completable attribute that triggers the following directive:
app.directive('engCompletable', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// add the id of this element to the completables array in the main controller
scope.$parent.addCompletable(attrs.id);
}
}
});
So every time angular encounters an 'eng-completable' attribute on an element, it calls addCompletable on the parent scope which adds the element id to the 'completables' array and 'false' to the corresponding index of the 'completed' array.
In the eng-popup attribute directive, we have a function to check if it has been made visible:
app.directive('engPopup', function() {
return {
restrict: 'A',
replace: true,
templateUrl: 'components/popup.html',
link: function(scope, element, attrs) {
scope.$watch(function() { return element.is(':visible') }, function() {
scope.$parent.componentCompleted(attrs.id);
});
}
};
});
Which also uses the parent scope to trigger the 'componentCompleted' function. I've been told that referring to the parent scope is bad practise, and it is also messing up our unit tests, apparently.
I'd like to know what is the alternative. How can I let my app know that a specific component has been completed? And where should this state be tracked?
I'd really like to know HOW to do this - not just be told that I'm doing it the wrong way. Please let me know what the alternative is.
But, as always, any help will be much appreciated.
One alternative would be to create a Service to be responsible to track all the components and keep their states (complete/not completed).
It will remove the need for $scope.parent and the service can be injected into any controller or directive you need.
:)
If that completables list is application-wide yo could consider adding it to your $rootScope along with the addCompletable method —and any other relate methods— instead of adding it to your mainController's $scope.
This way you could substitude your scope.$parent.componentCompleted(attrs.id); with $rootScope.componentCompleted(attrs.id); and avoid to make calls to scope.$parent.

What is angularjs $compile double parenthesis

I 'm a bit worried If I am asking a noob question or if it is a javascript feature that I haven't been able to locate despite plenty of googling
I am adding a simple directive programmatically using $compile and all is working fine.
My question is this line
var el = $compile(newElement)($scope);
How do the double parenthesis work/ what do they do? Complete code below for reference but its just the parenthesis which I am not sure about.
var myApp = angular.module('myApp', []);
myApp.directive('myDirective', function() {
return {
template: 'Hello',
restrict: 'E'
}
});
myApp.controller('mainController', [
'$scope', '$compile', function($scope, $compile) {
$scope.addDirective = function() {
var newElement = angular.element(document.createElement('my-directive'));
var el = $compile(newElement)($scope);
angular.element(document.body).append(el);
};
}
]);
$compile returns another function. You can do something similarly:
function foo(greeting) {
return function(target) { console.log(greeting, target) };
}
foo('Hello, ')('world');
As you already know that parenthesis in javascript is an function invocation operator (and also grouping). In other words, with () operator you call a function. From here it is clear that the code
$compile(newElement)($scope);
means that result of $compile(newElement) is a function, so it can be executed. This returned function accepts one parameter - a scope object in which context new DOM element should be compiled.
$compile(tElement, tAttrs, transclude) returns the Directive link: (post-link) function.
app.directive('exampleDirective', [function () {
return {
restrict: 'A',
scope: { value: '=value'},
template: template,
link: function (scope, element, attr) {
scope.count = 0;
scope.increment = function() {
scope.value++;
};
}
};
}]);
In this case, $compile('<div example-directive></div>'); will return the link: function so you can call it with arguments (scope as first) and instanciate the context.
It's all just standard Javascript syntax for calling functions. What might be confusing is that $compile is a function that returns a function. So
$compile(newElement)
is itself function, and can be called like any other function, which is what's happening when writing
$compile(newElement)($scope);
You can break this up into separate lines if you wish, which might make it clearer:
var linkFunction = $compile(newElement);
linkFunction($scope);
You can refer to the usage of $compile in the docs.
As a side-note, I would be wary of using $compile directly: you might be overcomplicating things, and there could be simpler alternatives (it has been very rare that I've ever had to use it).

Getting directive name in AngularJS

I've got an Angular directive. Inside the link function, I do this:
link: function(scope, element, attrs) {
...
element.data('startY', value);
...
}
What I'd like to do is perfix 'startY' with the name of the directive, without hard-coding the name. I'd like to dynamically get the name.
Is there any way to do this? Does Angular provide a way to reflect it? Something like:
link: function(scope, element, attrs) {
...
element.data(this.$name + '-startY', value);
...
}
If not, what are the recommended best practices for choosing data() keys to avoid collisions?
As indicated in the AngularJS source code, a directive's name is assigned in the context of the object literal where your directive options reside. The link function however cannot access the object literal's context, this, because it will be transferred to a compile function where it will be returned and invoked after the compilation process has taken place.
To get the name within your link function you can follow any of these suggestions:
[ 1 ] Create a variable that may hold reference to the object literal(directive options).
.directive('myDirective', function() {
var dir = {
link: function(scope, elem, attr) {
console.log(dir.name);
}
};
return dir;
});
[ 2 ] You can also get the directive's name by using the compile function since it is invoked in the context of the directive option.
.directive('myDirective', function() {
return {
compile: function(tElem, tAttr) {
var dirName = this.name;
return function(scope, elem, attr) { /* link function */ }
}
};
});
As far as I can tell you've answered your own question. You may prefix the name by string concatenation as you've done but it will probably be easier to add it as a separate data store.
element.data('directiveName', this.$name).data('startY', value);
I'm not sure what you mean by avoid collision as this will only apply to the element that was passed into the link function.

Modify template in directive (dynamically adding another directive)

Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>

Categories