ngShow don't work as expected when using custom directive - javascript

I have this code in angular:
<span ng-mouseover="item.show_description=true" ng-mouseleave="item.show_description=false" pointer="{x: item.x, y: item.y}">
{{item.label}}
</span>
<div class="description-popup" ng-show="!!item.description && item.show_description"
style="left: {{item.x}}px; top: {{item.y}}px">
<h2>{{item.label}}</h2>
<p>{{item.description}}</p>
<p>{{!!item.description && item.show_description}}</p>
</div>
It show popup correctly but if descritpion is null or empty string the popup still shows up. The last expression in that case show false. What I'm doing wrong here? Or maybe is there a bug there. I'm using Angular 1.0.6 (can't upgrade right now).
UPDATE:
I've create JSFiddle and it's seems that ng-show work as expected but not when I use pointer directive, that use mousemove event. The code for the directive is like this:
app.directive('pointer', function($parse) {
function objectParser(expr) {
var strip = expr.replace(/\s*\{\s*|\s*\}\s*/g, '');
var pairs = strip.split(/\s*,\s*/);
if (pairs.length) {
var getters = {};
var tmp;
for (var i=pairs.length; i--;) {
tmp = pairs[i].split(/\s*:\s*/);
if (tmp.length != 2) {
throw new Error(expr + " is Invalid Object");
}
getters[tmp[0]] = $parse(tmp[1]);
}
return {
assign: function(context, object) {
for (var key in object) {
if (object.hasOwnProperty(key)) {
if (getters[key]) {
getters[key].assign(context, object[key]);
}
}
}
}
}
}
}
return {
restrict: 'A',
link: function(scope, element, attrs) {
var expr = objectParser(attrs.pointer);
element.mousemove(function(e) {
var offest = element.offset();
scope.$apply(function() {
expr.assign(scope, {
x: e.pageX - offest.left,
y: e.pageY - offest.top
});
});
});
}
};
});

Isolate scope
The problem is that ng-show uses the scope of the element it's on. If it's on an element that has a directive with isolate scope, then it will use that isolate scope, and not have access to the outside scope.
In this particular case, I suspect that description-popup has isolate scope, which means ng-show only has access to the scope of that directive, which probably doesn't have the item that you're trying to test against.
What is isolate scope?
Isolate scope means the directive has its own scope that does not inherit from the surrounding scope. Normal scopes inherit from the surrounding scopes, so they have access to the data on that surrounding scope. Isolate scope does not.
Why would anyone ever want to use isolate scope?!
Reuse. If a directive is intended to be reused in lots of places, in lots of different scopes with a variety of properties on them, you don't want properties on the directive's scope to collide with the properties of the surrounding scope. And since the directive is supposed to be totally independent and not use the surrounding scope at all, it's often a good idea to give it an isolate scope. There are very few disadvantages to this. Really, the only thing that's likely to go wrong is when someone wants to put an ng-show or ng-hide on that element.
Solution
Put your ng-show on an extra <div> around the description-popup:
<div ng-show="!!item.description && item.show_description">
<div class="description-popup"
style="left: {{item.x}}px; top: {{item.y}}px">
<h2>{{item.label}}</h2>
<p>{{item.description}}</p>
<p>{{!!item.description && item.show_description}}</p>
</div>
</div>
This assuming that the behaviour in this JSFiddle is what you want.
This is probably only true for versions of Angular < 1.2. In 1.2, the behaviour of isolate scopes seems to have been cleaned up: https://docs.angularjs.org/guide/migration#isolate-scope-only-exposed-to-directives-with-scope-property

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.

Understanding one-way binding with angular 1.5

There is one thing I don't understand with one-way bindings demonstrated by the code and demo below:
var example = {
bindings: {
obj: '<',
prim: '<'
},
template: `
<div class="section">
<h4>
Isolate Component
</h4>
<p>Object: {{ $ctrl.obj }}</p>
<p>Primitive: {{ $ctrl.prim }}</p>
<a href="" ng-click="$ctrl.updateValues();">
Change Isolate Values
</a>
</div>
`,
controller: function () {
this.updateValues = function () {
this.prim = 10;
this.obj = {
john: {
age: 35,
location: 'Unknown'
}
};
};
}
};
function ParentController() {
this.somePrimitive = 99;
this.someObject = {
todd: {
age: 25,
location: 'England, UK'
}
};
this.updateValues = function () {
this.somePrimitive = 33;
this.someObject = {
jilles: {
age: 20,
location: 'Netherlands'
}
};
};
}
angular
.module('app', [])
.component('example', example)
.controller('ParentController', ParentController);
DEMO
The thing I don't understand is how the primitive value changes (or doesn't change) if you modify it in the isolated scope. As far as I understand if you one-way bind a primitive, the binding will break if you change that value within the isolated scope. With break I mean that an update by the parent will not propagate anymore into the isolated scope.
However, in the demo it doesn't exactly work like that.
HOW TO REPRODUCE: Go to the DEMO and first click the Change isolate values button. This will change the primitive in the isolated scope. Next click the Change Parent Values button. As you can see the primitive within the isolated scope is still updated. Based on this, I guess the binding doesn't break if you update a primitive inside the isolated scope, right?! But now things get weird. If you repeat this, you'll notice that this time the binding doesn't work anymore.
Can someone explain why the primitive value did change the first time but didn't the second time ?
UPDATE: Found the solution. The binding works perfect, because the value of the parent doesn't change anymore, its always 10. Here is an updated example which shows that it still works

accessing rootScope in directive

I've tried a million ways to access a rootScope variable from within this elasticui based directive with zero luck. Here is the directive, bearing in mind $rootScope is indeed being passed into its parent controller.
var elasticui;
(function (elasticui) {
var directives;
(function (directives, rootScope) {
var IndexDirective = (function () {
function IndexDirective() {
var directive = {};
directive.restrict = 'EAC';
directive.controller = elasticui.controllers.IndexController;
directive.link = function (scope, element, attrs, indexCtrl, rootScope) {
console.log("Updated Index: " + $rootScope.esIndex);
indexCtrl.indexVM.index = scope.$eval(attrs.euiIndex);
};
return directive;
}
return IndexDirective;
})();
directives.IndexDirective = IndexDirective;
directives.directives.directive('euiIndex', IndexDirective);
})(directives = elasticui.directives || (elasticui.directives = {}));
})(elasticui || (elasticui = {}));
Elsewhere I'm setting $rootScope.esIndex to the index I'd like to point to but I'm getting "$rootScope is not defined". I've tried setting the directive's scope property to false, tried setting up a watcher on $rootScope.esIndex as part of the link functions return value but no matter what I do, I can't seem to figure it out.
I think part of the problem for me is this structure is a little cryptic from a directive standpoint for a newer angular person to digest. This is a block in a far larger set of directive definitions so it's just hard for me to grasp.
Any ideas how I can easy grab $rootScope in here?
Thanks a TON in advance!
Seeing more of your code would help. Specifically when you say "$rootScope is indeed being passed". However, with this limited info why don't you try changing this line:
(directives = elasticui.directives || (elasticui.directives = {}));
to something like
(directives = elasticui.directives || (elasticui.directives = {}, rootScope));
At a glance it appears your self executing function isn't being passed the rootScope parameter it expects. Good luck!
edit: Also
console.log("Updated Index: " + $rootScope.esIndex);
Should probably not have the dollar sign.
edit: Comments have much more info!

One way binding to an object in angular

I'd like to have a one-way (not one time) binding between an attribute on a directive, but i'm struggling with how to express this without attrs.$observe. The best I can come up with at the moment is to bind via &attr and invoke the variables I am binding to in my template e.g. {{attr()}}
app.controller('MainCtrl', function($scope) {
$scope.names = ['Original'];
setTimeout(function () {
$scope.names.push('Asynchronously updated name');
$scope.$apply();
}, 1000);
});
app.directive('helloComponent', function () {
return {
scope: {
'names': '&names'
},
template: '<li ng-repeat="name in names()">Hello {{name}}</li>'
}
});
<body ng-controller="MainCtrl">
<ul>
<hello-component names="names"/>
</ul>
</body>
Plunker
Is there a better way to do this that preserves the one-way binding without the need to invoke the bound properties?
Edit
I've updated the example code to clarify that I want to bind to an object, not just a string. So #attr (which works with a string attribute) is not a solution.
The "&" is actually the right thing to do. I have argued against this approach (with #JoeEnzminger, here and here) on the basis that it is semantically questionable. But overall Joe was right - this is the way to create a one-way binding to an actual object vs. "#" which binds to a string.
If you don't fancy an isolate scope, then you could get the same effect by using $parse:
var parsedName = $parse(attrs.name);
$scope.nameFn = function(){
return parsedName($scope);
}
and use it in the template as:
"<p>Hello {{nameFn()}}</p>"
I didn't see any mention of it in the other answers, but as of Angular 1.5, one-way bindings for objects are supported (see scope section in $compile docs for Angular 1.5.9):
< or <attr - set up a one-way (one-directional) binding between a local scope property and an expression passed via the attribute attr. The expression is evaluated in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. You can also make the binding optional by adding ?: <? or <?attr.
For example, given <my-component my-attr="parentModel"> and directive definition of scope: { localModel:'<myAttr' }, then the isolated scope property localModel will reflect the value of parentModel on the parent scope. Any changes to parentModel will be reflected in localModel, but changes in localModel will not reflect in parentModel. There are however two caveats:
one-way binding does not copy the value from the parent to the isolate scope, it simply sets the same value. That means if your bound value is an object, changes to its properties in the isolated scope will be reflected in the parent scope (because both reference the same object).
one-way binding watches changes to the identity of the parent value. That means the $watch on the parent value only fires if the reference to the value has changed. In most cases, this should not be of concern, but can be important to know if you one-way bind to an object, and then replace that object in the isolated scope. If you now change a property of the object in your parent scope, the change will not be propagated to the isolated scope, because the identity of the object on the parent scope has not changed. Instead you must assign a new object.
One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings back to the parent. However, it does not make this completely impossible.
In the example below, one-way binding is used to propagate changes in an object in the scope of a controller to a directive.
Update
As pointed out by #Suamere, you can indeed change properties of a bound object with one-way-binding; however, if the whole object is changed from the local model, then the binding with the parent model will break, as the parent and local scope will be referring to different objects. Two-way binding takes care of this. The code snippet was updated to highlight the differences.
angular.module('App', [])
.directive('counter', function() {
return {
templateUrl: 'counter.html',
restrict: 'E',
scope: {
obj1: '<objOneWayBinding',
obj2: '=objTwoWayBinding'
},
link: function(scope) {
scope.increment1 = function() {
scope.obj1.counter++;
};
scope.increment2 = function() {
scope.obj2.counter++;
};
scope.reset1 = function() {
scope.obj1 = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "One-way binding",
creator: "Directive"
};
};
scope.reset2 = function() {
scope.obj2 = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "Two-way binding",
creator: "Directive"
};
};
}
};
})
.controller('MyCtrl', ['$scope', function($scope) {
$scope.increment = function() {
$scope.obj1FromController.counter++;
$scope.obj2FromController.counter++;
};
$scope.reset = function() {
$scope.obj1FromController = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "One-way binding",
creator: "Parent"
};
$scope.obj2FromController = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "Two-way binding",
creator: "Parent"
};
};
$scope.reset();
}])
;
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.js"></script>
<div ng-app="App">
<script type="text/ng-template" id="counter.html">
<h3>In Directive</h3>
<pre>{{obj1 | json:0}}</pre>
<button ng-click="increment1()">
Increment obj1 from directive
</button>
<button ng-click="reset1()">
Replace obj1 from directive (breaks binding)
</button>
<pre>{{obj2 | json:0}}</pre>
<button ng-click="increment2()">
Increment obj2 from directive
</button>
<button ng-click="reset2()">
Replace obj2 from directive (maintains binding)
</button>
</script>
<div ng-controller="MyCtrl">
<counter obj-one-way-binding="obj1FromController"
obj-two-way-binding="obj2FromController">
</counter>
<h3>In Parent</h3>
<pre>{{obj1FromController | json:0}}</pre>
<pre>{{obj2FromController | json:0}}</pre>
<button ng-click="increment()">
Increment from parent
</button>
<button ng-click="reset()">
Replace from parent (maintains binding)
</button>
</div>
</div>
Doing an attribute literally passes a string. So instead of doing this:
<hello-component name="name"/>
You can do this:
<hello-component name="{{name}}"/>
This may be essentially the same approach proposed by New Dev, but I solved a similar problem for myself by taking an object off of my isolate scope and creating a getter function for it which called scope.$parent.$eval(attrs.myObj).
In a simplified version that looks more like yours I changed:
app.directive('myDir', [function() {
return {
scope : {
id : '#',
otherScopeVar : '=',
names : '='
},
template : '<li ng-repeat="name in names">{{name}}</li>'
}
}]);
to
app.directive('myDir', [function() {
return {
scope : {
id : '#',
otherScopeVar : '='
},
template : '<li ng-repeat="name in getNames()">{{name}}</li>',
link : function(scope, elem, attrs) {
scope.getNames() {
return scope.$parent.$eval(attrs.myList);
};
}
}
}]);
That way whenever a digest runs your object is pulled as is from the parent scope. For me, the advantage to doing it this way was that I was able to change the directive from two-way to one-way binding (which took my performance from unusable to working fine) without changing the views that used the directive.
EDIT
On second thought I am not sure this is exactly one-way binding, because while updating the variable and running a digest will always use the updated object, there is no inherent way to run other logic when it changes, as one could with a $watch.

Why is my angular directive not getting Parent controller scope inside isolated scope?

I've followed Angular docs precisely to get a directive working with an isolated scope containing a couple of vars from the parent Controller's scope object.
app.controller('MainCtrl', function($scope) {
$scope.name = 'Parent Name';
$scope.pokie = {
whyIs: "thisUndefined?"
};
});
app.directive('parseObject', function() {
var preLink = function($scope, el, att, controller) {
console.log('[link] :: ', $scope);
};
var postLink = function($scope, el, att, controller) {
console.log('[PostLink] :: ', $scope);
console.log('[$Parent] :: ', $scope.$parent.name);
};
return {
restrict: 'E',
scope: {
myPokie: '=pokie',
name: '=name'
},
template: [
'<div>',
'<h1>Directive does not get parent scope</h1>',
'<h1>{{ myPokie }}</h1>',
'<h2>{{ name }}</h2>',
'</div>'
].join(''),
compile: function() {
return {
pre: preLink,
post: postLink
}
}
}
});
http://plnkr.co/edit/FpQtt9?p=preview
Can anyone tell me what is wrong with my code? Why does the directive's Isolate scope return undefined values for 'myPokie' and 'name'?
I've seen other people saying you have to use $scope.watch for this.. but angular's directive docs don't say anything about that.. And I really don't want to use $scope.watch for something so trivial which should work out of the box.
As you declared isolated scope in your directive, that means you are going to provide those value to your directive using attribute.
scope: {
myPokie: '=pokie',
name: '=name'
}
This means your directive scope will not be prototypically inherited from the parent scope. pokie attribute provide the value for myPokie & name attribute will provide value of name for your directive, = indicating a two way binding if your myPokie value change in directive, the same referencing value will change in the parent controller. The same is true for the name attribute.
Your directive element markup should be:
<parse-object pokie="pokie" name="name"></parse-object>
Working Plunkr
You're using isolated scope but you're not passing the variables. The problem is in your HTML:
Change your HTML from:
<parse-object></parse-object>
To:
<parse-object pokie="pokie" name="name"></parse-object>
Isolated scope takes its parameters from the DOM element. So if you have inside the scope declaration:
myPokie: '=pokie',
That means that myPokie variable should be taken from the pokie attribute that's on the scope. Your name: "=name" can be changed to name: "=" since it is exactly the same name.
Plunker

Categories