Isolate Scope "=" binding and doted notation AngularJS - javascript

How do you create a 2 way binding with a nested property in an isolate scope with dotted notation. I thought 'myObject.data': "=data" would work, but it does not. I don't want to link everything in the myObject object. I know I could do some sort of watch, but 'myObject.data' seems cleaner.
.directive("myDirective", [function() {
return {
restrict: "E",
scope: {
'myObject.data': "=data"
},
link: function (scope, element, attrs) {
scope.myObject = {
data: "myValue"
};
}
};
}])

Isolated scopes are generally useful only with templates, they should not be used as a way to declare how you want your directive attributes to be interpreted. This is because most directives that don't have a template usually need the semantics of either a child scope or the direct scope of their environment.
In your case, you probably don't even need a $watch, because object references are what enable 2 way data binding, but without your full code I cannot be sure.
In case you want to know the translations for an isolated scope semantics to just a normal one:
#name -> attrs.name
=name -> $scope.$eval(attrs.name);
&name -> function() { return $scope.$eval(attrs.name); }
EDIT 2:
After your comment, I came up with this plunker. To preserve two way data binding you have to use a "." in your ng-model declaration. This is because two way data binding does not work for value types, since they are immutable. You can't change the value of 100 for example. You need to pass around a reference type object and hang the values you are changing off of it. Your desire to specify the full path to the value in the isolated scope definition is not possible based on the principles that two way data binding is made possible by.
Javascript:
angular.module('plunker', [])
.directive('twoWay', function() {
return {
restrict: 'E',
template: '<div><input ng-model="thing.name" type="text" /></div>',
scope: {
thing: "="
},
link: function(scope, element, attrs) {
}
};
})
.controller('MainCtrl', function($scope) {
$scope.data = {
name: "World"
};
});
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{data.name}}!</p>
<two-way thing="data"></two-way>
</body>

What I use in these cases is the following:
.directive("myDirective", [function() {
return {
restrict: "E",
scope: {
data: "="
},
controller: function($scope){
$scope.dot = $scope //<--- here is the trick
}
};
}])
Then you can always change data in the directive's scope from an inherited scope through dot.data = 'whatever' without setting watchers.
Not very elegant but it works jsut fine in cases where you are not using the controller as syntax and don't want a $parent nightmare.

Related

angularjs directive use parameter inside link function

This is probably a stupid question, but I have been stuck on it for days and neither of the googled solutions panned out for me.
I'm writing an angular 1.4 app following a directive driven approach, so for any entity I have one or more widgets:
isolated scope
controller as
bindToController true
The problem is of the "user has stuff" variety.
<user-widget>
<stuff-widget userid="user._id"></stuff-widget>
</user-widget>
The userid passes nicely into the widget, but I would like to use it inside the stuff widget's link function, because stuff is really complicated and in the real world I need to grab various other parts as well.
I tried various methods to get to the userid value (as suggested in various other stackoverflow discussions and elsewhere on the web)
use bidrectional binding -> not effective
tried via scope and controller -> userid, uid not defined
require ^userWidget -> lost access to my controller
use attrs.$observe('userid', ....) -> js error: userid not defined
passed it via pre link of the parent -> did work, but not a good idea/limited merits
I have a plunker with the various things I tried: http://plnkr.co/edit/SqlhYSteCDxMaAVZWCsy?p=info
The widgets look like this (working pre link variant)
function userWidget() {
return {
restrict: 'EA',
scope:{},
template: '<h2>User</h2> {{user.name}}<stuff-widget userid="user._id"></stuff-widget>',
replace: false,
controllerAs: 'userCtrl',
bindToController: true,
controller: 'UserCtrl',
link: { // this actually works, not sure wether this a good idea
pre: function preLink(scope, element, attrs, ctrl) {
var uid = 1;
scope.user = ctrl.findById(uid);
scope.userid = scope.user._id;
},
post: function postLink(scope, element, attrs, ctrl) {}
}
};
}
function stuffWidget() {
return {
restrict: 'EA',
scope: {
uid: '=userid'
},
template: '<h3>User stuff</h3>stuffCtrl.userid inside widget: {{stuffCtrl.userid}}<div ng-repeat="stuff in stuffCtrl.userStuff">\
User id: {{stuff.userid}}: {{stuff.stuff}}\
</div>',
replace: false,
controller: 'StuffCtrl',
controllerAs: 'stuffCtrl',
bindToController: {
userid: '='
},
link: function (scope, element, attrs, ctrl) {
console.log('stuff ctrl userid:', ctrl.userid); // not defined
console.log('stuff scope uid:', scope.uid); // not defined
console.log('Scope parent userid: ',scope.$parent.userid); // undefined
// didn't work either - userid not defined
/* attrs.$observe('userid', function(value) {
if (value) {
ctrl.userid = userid;
console.log('stuff ctrl userid observed:', ctrl.userid);
}
}); */
ctrl.userStuff = ctrl.findByUserId(ctrl.userid);
}
};
}
I'm an angular beginner (first serious project) and so far my experience has been: "if it's hard to figure out, you are probably doing it the wrong way".
So,
Did I miss an obvious way to access the param inside the link function?
Did I screw up the ways I tried (especially $observe) because I incorrectly transferred them to my setting?
Should I just stick the controller calls inside the template and be done with it?
Should I go about it in an entirely different way? compile function? controller function? whatever other angular depths I'm not yet familiar with?
After the link function has finished its execution you will have to wait one digest cycle to update the values in the isolated scope from the scope where the directive is used.
You can try this snippet in stuffWidged:
link: function(scope, element, attrs, ctrl) {
var uid, usrStuff;
scope.uid; // it is normal to be undefined here
scope.$watch('uid', function(newUid) {
uid = newUid;
scope.uid; // is ready and is same as newUid
usrStuff = ctrl.usrStuff = scope.usrStuff =
ctrl.findById(uid);
});
}
Remove isolated scope from user-widget - do u really need it there? If you are not defining any additional variables in directive scope - you do not actually need isolated scope.
Isolated scope. So user-widget should have some parameters? Like:
<user-widget widgetusers="model.users"></user-widget>
then user-widget template will be:
<stuff-widget ng-repeat="stuffuser in widgetusers" userid="stuffuser._id"></stuff-widget>
You can pass userId first to userWidget, then to stuffWidget:
<user-widget userid="user._id">
<stuff-widget userid="userid"></stuff-widget>
</user-widget>

angularjs directive: how communicate between link and controller?

I have a directive whose 'config' attribute value I need to access inside my directive controller.
Since the controller constructor get executed first,communication from controller to link is possible but not vice versa.
What should be the best way to achieve this?
I have considered the following approaches
1)Add the variable to scope-
That would in my opinion pollute the scope,making the variable accessible every where else where the scope is being shared.
2)Use $broadcast
Again the same issue as above
3) Pass a callback function on controller's this and call it from the link function with config as its argument
4)Pass the value through a service- In my case I have multiple such directives that would need to pass date through this service
Or is there some better approach that I am missing out for doing this?
module.directive('myDirective',function(){
return{
restrict:'E',
templateUrl:'path/to/html',
link:function(scope,iElement,iAttrs,controller){
var config=iAttrs.config;
//How to access this value inside the directive controller?
},
controller:function($scope){
//the directive attribute 'config' is required here for some larger computations which are not
//manipulating the DOM and hence should be seperated from the link function
})
There you can use isolated scope concept where you create isolated scope inside your controller & that would not be prototypically inherited from its parent scope. For that you need to use scope: { ... } inside your directive option. There are three options to pass scope value inside a directive through attribute
# : One way binding
= : Two way binding
& : Expression
In your case first two cases would be fine that are depends which one you need to use. If you just want to pass the value of scope variable to the directive in that case you could use 1st approach which would be # one way binding.
If you want to update the variable in both directive as well as controller from where it come i.e. nothing but two way binding, then you need to use =
I think = suits in your case so you should go for =
Markup
<my-directive config="config"></my-directive>
Directive
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
config: '='
},
templateUrl: 'path/to/abc.html',
link: function(scope, iElement, iAttrs, controller) {
//here it will be access as scope.config
console.log(scope.config);
},
controller: function($scope) {
console.log($scope.config); //here also it would be available inisde scope
//you could put a watch to detect a changes on config
}
}
});
Demo Plunkr
Update
As config value has been provide from the attribute with expression like {{}} so we could get those changes inside controller by putting [**$observe**][2] on $attrs. For that you need to inject $attrs dependency on your controller that will give you all the attributes collection which are available on directive element. And on the same $attrs object we gonna put $observe which work same as that of $watch which does dirty checking & if value gets change it fires that watch.
Directive
app.directive('myDirective', function() {
return {
restrict: 'E',
templateUrl: 'path/to/abc.html',
link: function(scope, iElement, iAttrs, controller) {
//here it will be access as scope.config
console.log(scope.config);
},
controller: function($scope,$attrs) {
//you could put a watch to detect a changes on config
$attrs.$observe('config', function(newV){
console.log(newV);
})
}
}
});
Updated Demo

Outer AngularJS directive doesn't build inner directive correctly

Using AngularJS 1.0.8, I'm trying to create some reusable directives to create a situation where a web developer can code a single "top-level" directive with a number of attributes, and this directive, in turn, has a template containing other directives, which themselves might contain other directives etc.
The problem I'm having is making the "inner" templates aware of the top level attributes. I thought this would be a universal problem, but it didn't look, from my research, that anyone else was asking this.
I created this Plunker to show the problem:
<!DOCTYPE html>
<html ng-app="outerInnerDirectivesApp">
<head>
<title>Outer/Inner Directives</title>
</head>
<body>
<div>Single level directive follows:</div>
<single-level-directive single-level-id="single123"></single-level-directive>
<div>Outer/inner directive follows (Expecting "outer123"):</div>
<outer-directive outer-id="outer123"></outer-directive>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
<script src="app.js"></script>
<script src="directives.js"></script>
</body>
</html>
In the Plunker,
single-level-directive works and is, I think, a standard way to display data.
outer-directive and inner-directive aren't working.
What I expected to happen with these was
(i) outerDirective compiles/links to produce the html
<inner-directive inner-id="outer123"></inner-directive>
and then
(ii) innerDirective compiles/links to produce html
<div>outer123</div>
But at step (ii) I get
<inner-directive inner-id="" class="ng-isolate-scope ng-scope">
<div class="ng-binding"></div>
</inner-directive>
so an empty div is generated by innerDirective.
In fact, if I change outer-template.html to be
<div>{{outerId}}<div>
then the value displays correctly, so it looks like scope.outerId is available at the correct point, but Angular isn't happy about me trying to use it in the way I am.
Is this a reasonable thing to expect Angular to do? If so, what am I missing? If not, then what do you think would be a sensible alternative way to build up more complex screens from simple sets of directives?
If you are going to design directives with isolated scope, I would suggest using the isolated scope to define the type of attribute you want to use:
outerInnerApp.directive("outerDirective", function() {
return {
restrict: "E",
scope: {
outerId: '#'
},
link: function(scope, element, attrs) {
},
templateUrl: "outer-template.html"
};
});
outerInnerApp.directive("innerDirective", function() {
return {
restrict: "E",
scope: {
innerId: '='
},
link: function(scope, element, attrs) {
},
templateUrl: "inner-template.html"
};
});
Here is a working plunker.
Your outer directive is using the value that is defined in the attribute. So, to pass the value into the isolated scope, we can use #. The inner scope is then binding a variable through. So, we can use = to set up a bound attribute.
Had some more thoughts about this. Having used AngularJS a bit more, I'm not sure that I want to bind to the scope (using the "="). In fact, I can get the original Plunkr to work by making these changes:
outerInnerApp.directive("outerDirective", function() {
return {
restrict: "E",
scope: {
//add outerId here
outerId: "#"
},
link: function(scope, element, attrs) {
//remove scope assignment here
//scope.outerId = attrs.outerId;
},
templateUrl: "outer-template.html"
};
});
outerInnerApp.directive("innerDirective", function() {
return {
restrict: "E",
scope: {
//add innerId here
innerId: "#"
},
link: function(scope, element, attrs) {
//remove scope assignment here
//scope.innerId = attrs.innerId;
},
templateUrl: "inner-template.html"
};
});
What I don't understand at the moment is why there is a different between, say,
innerId:"#"
and setting the value of scope in the link function
link: function(scope, element, attrs) {
scope.innerId = attrs.innerId;
}
When I find out why it behaves differently I'll post back.

Directive behavior changes when defining isolated scope

I have a directive for a javascript grid library slickgrid.
http://plnkr.co/edit/KWZ9i767ycz49hZZGswB?p=preview
What I want to do is pass the selected row back up the controller. So I want to use isolated scope (using the '=') to get two-way binding working between the controller and directive.
Everything works if I define the directive without any sort of scope declaration:
<slickgrid id="myGrid" data="names" selected-item="selectedItem"></slickgrid>
app.directive('slickgrid', function() {
return {
restrict: 'E',
replace: true,
//scope: {
// selectedItem: '='
//},
template: '<div></div>',
link: function($scope, element, attrs) {
...
var redraw = function(newScopeData) {
grid.setData(newScopeData);
grid.render();
};
$scope.$watch(attrs.data, redraw, true);
But if I uncomment the lines above (lines 19-21 in app.js) it looks like the $scope.$watch which is watching the attrs.data object is calling redraw but the attrs.data is being passed in as undefined.
My analysis could be wrong, but I'm not sure why defining the scope would cause this. Can someone explain why that might be?
.nathan.
If you define an isolate scope, then any $watch in your directive will be looking for whatever attrs.data evaluates to on your isolate scope. attrs.data evaluates to the string names, so the $watch is looking for $scope.names on your isolate scope, which doesn't exist. (Without the isolate scope, the directive uses the same scope as MainCtrl, and $scope.names exists there.)
To use an isolate scope, you'll need to define another isolate scope property to pass in names:
scope: {
selectedItem: '=',
data: '='
},
...
$scope.$watch('data', redraw, true);
The HTML can remain the same.

Scoping issue when setting ngModel from a directive

I have a directive which looks something like:
var myApp = angular.module('myApp',[])
.directive("test", function() {
return {
template: '<button ng-click="setValue()">Set value</button>',
require: 'ngModel',
link: function(scope, iElement, iAttrs, ngModel) {
scope.setValue = function(){
ngModel.$setViewValue(iAttrs.setTo);
}
}
};
});
The problem is that if I use this directive multiple times in a page then setValue only gets called on the last declared directive. The obvious solution is to isolate the scope using scope: {} but then the ngModel isn't accessible outside the directive.
​
Here is a JSFiddle of my code: http://jsfiddle.net/kMybm/3/
For this scenario ngModel probably isn't the right solution. That's mostly for binding values to forms to doing things like marking them dirty and validation...
Here you could just use a two way binding from an isolated scope, like so:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
target: '=target',
setTo: '#setTo'
},
template: '<button ng-click="setValue()">Set value</button>',
controller: function($scope) {
$scope.setValue = function() {
$scope.target = $scope.setTo;
};
//HACK: to get rid of strange behavior mentioned in comments
$scope.$watch('target',function(){});
}
};
});
All you need to do is add scope: true to your directive hash. That makes a new inheriting child scope for each instance of your directive, instead of continually overwriting "setValue" on whatever scope is already in play.
And you're right about isolate scope. My advice to newbies is just don't use it ever.
Response to comment:
I understand the question better now. When you set a value via an expression, it sets it in the most immediate scope. So what people typically do with Angular is they read and mutate values instead of overwriting values. This entails containing things in some structure like an Object or Array.
See updated fiddle:
http://jsfiddle.net/kMybm/20/
("foo" would normally go in a controller hooked up via ngController.)
Another option, if you really want to do it "scopeless", is to not use ng-click and just handle click yourself.
http://jsfiddle.net/WnU6z/8/

Categories

Resources