Deleting a referenced scope object from inside an isolated scope - angular.js - javascript

I have an angular directive uses the = operator to two-way bind an isolated scope with an attribute of its parent scope:
app.directive('nestedDirective', function(){
return {
scope: {
model: '='
},
link: function($scope){
...
}
}
});
I understand that any changes to $scope.model will propagate to the parent scope. However, deleting $scope.model does not propagate. delete($scope.model) My question thus: How do I delete the referenced variable and propagate the removal to the parent scope.
This codepen should illustrate what I'm trying to do not (Not even watching the scope fires an event)

This question gets asked very, very often, so I'll start by referring to the wiki article.
Basically, follow the "dot rule" : if you need to modify a property (directly), scope it under another property so that JS prototypal inheritance can kick in :
var model = {prop: "val"};
var a = {model: model};
model = null;
console.log(a.model.prop); // prints val
var b = {a: a};
a.model = null;
console.log(b.a.model); // prints null
This is the same here (even if this doesn't use a prototypal inheritance to keep things simple).

I've edited your code pen source below, i'm sure there's a simpler way of doing this but i've just tried this and it works, it should start you on the right path:
<ul ng-app="app" ng-controller="ctrl">
<dir model="data.children" child="child" ng-repeat="child in data.children"></dir>
</ul>
var app = angular.module('app', []);
app.controller('ctrl', function($scope){
$scope.data = {};
$scope.data.children = [
{name: 'Ben'},
{name: 'Heffler'},
{name: 'Schubert'}
];
$scope.$watchCollection('data.children', function(){
console.log("children shallow watch", $scope);
});
$scope.$watch('data.children', function(){
console.log("children deep watch",$scope);
}, true);
});
app.directive('dir', function(){
return {
restrict: 'E',
scope: {
model: '=',
child:'='
},
replace: true,
template: '<div>{{child.name}} <button ng-click="remove()">Remove</button></div>',
link: function(scope, element, attrs){
scope.remove = function(){
// I'm just deleting the first one as an example.
delete(scope.model[0]);
console.log("children inner scope", scope)
}
}
};
});
I'm not sure why you would want to delete the properties but i'm sure you have your reasons, just to show you it is possible.
EDIT
Here is the edited code pen (see the console logs to see the deleted items in scope). http://cdpn.io/Ghmvk

Related

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

AngularJs: Passing Function Arguments Across Multiple Isolated Scopes with Angular Directives

I have 3 nested, isolate scope, directives (see CodePen here) and I am able to pass a function (that takes an argument) from the outermost directive to the innermost directive (passing the function from outer directive to intermediate directive to inner directive).
What I'm failing to understand is what needs to be done to pass the argument from the inner directive back through intermediate directive back to the outer directive.
Again, see the CodePen example.
Note: Given only 2 isolate scope directives I can get this to work with something similar to the following...
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
The above code gives me an Alert box with 'Henry' (just like I'd expect).
It's when I add an third, intermediate, isolate scope directive that I run into problems...
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
template: '<div><my-dir-3 add-name="addName({name: testName})"></my-dir-3></div>',
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '&'
},
template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
This code gives me an alert box with undefined...
A common misconception is that "& is for passing functions". This isn't technically correct.
What & does is create a function on the directive scope that, when called, returns the result of the expression evaluated against the parent scope.
This function takes an object as an argument that will override local variables in the expression with those from the directive scope (the {name: testName}) object in this case.
If you were to look under the hood, the $scope.addName method in myDir2 would look like this (simplified):
$scope.addName = function(locals) {
return $parse(attr.addName)($scope.$parent, locals);
}
Your second directive works because the expression it is binding to is
addName(name)
This expression has a local variable name, that is overridden with the value of testName from the directive when executed with
addName({name: testName}) //in the directive.
Remember - the addName function in myDir2 IS NOT the same as the addName function in myDir1. It is a new function that evaluates the expression
addName(name)
against the parent scope and returns the result.
When you apply this logic to myDir3, the expression that is evaluated is:
addName({name: testName})
Note that the only local variable in this expression is "testName". So when you call in myDir3 with
addName({name: testName})
there is no local variable name to override, and testName is left undefined.
Phew! No wonder this confuses JUST ABOUT EVERYBODY!
How to fix in your example:
You want the expressions to evaluate to the actual function in myDir1.
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
// addName() returns the actual function addName from myDir1
template: '<div><my-dir-3 add-name="addName()"></my-dir-3></div>',
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '&'
},
//calls addName() on myDir2, which returns addName from myDir1, then invokes it passing testName as an argument
template: "<span ng-click='addName()(testName)'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
Here is the working Pen
Final note - the reason why '&' is more appropriate than '=' here is that '=' is going to actually set up a $watch and two-way bind the variables between the directives. This means that myDir2 could actually change the function appName in myDir1, which is not required and undesirable. It also requires setting up two $watchs, which I try to avoid for performance reasons in Angular.
There are two ways to pass a function in an isolated scope. While '&' will make sure that what you are passing is in-fact a function, you can also pass a function as a bound variable with '=', and invoke it only when you need. This method has drawbacks, but it will leave the control of the invocation to the component that is in-charge of that invocation.
Your codepen working
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name);
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '='
},
template: '<div><my-dir-3 add-name="addName"></my-dir-3></div>'
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '='
},
template: "<span ng-click='addName(testName)'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = "Henry"
}
}
});
The problem with your code with three nested directives is that testName does not exist in isolated scope of myDir2. You can set that from myDir3, but for that you will have to create an object in scope of myDir2 and set its property to testName and then use that property in myDir2
The working example is here
http://codepen.io/anon/pen/Wveqwx
'myDir2' when you have 3 directives is referencing 'testName', but it doesn't exist in that scope. It was mapped to 'name' in the 'myDir3'. Because of this, changing the template in myDir2 as follows, should fix it:
template: '<div><my-dir-3 add-name="addName({name: name})"></my-dir-3></div>'
You can access an elements scope using isolateScope()
In the myDir1 directive replace the controller to this instead
link: function($scope, elem) {
$scope.addName = function(name) {
var e = elem.find('my-dir-3');
var s = e.isolateScope();
alert(s.testName);
};
Another way is to potentially use $$nextSibling to get the next scope after current parent (haven't really used this so may want to read up on it)
This is probably not the 'angular way' to do things. I think removing the isolated scope from the child directives and having them reference the parent directive model would be more ideal.

AngularJS: Indicate the scope of a directive

I am building a directive that can be use in different controllers, and I would like to be able of bind the directive to a particular property of my $scope.
I would like to do something like this:
<div ng-app="myapp">
<div ng-controller="myController">
<my-directive-wrapper ng-model="mymodel">
<my-directive-inner ng-repeat="item in items" />
</my-directive-wrapper>
</div>
</div>
With this model:
$scope.mymodel = {
name : "Transclude test",
items : [
{ title : "test1" },
{ title : "test2" },
{ title : "test3" }
]
};
So the directive myDirectiveWrapper gets $scope.mymodel as scope, and nothing else. Then I may put the directive twice, pointing to a different property each.
I have a demo project with the problem here: http://jsfiddle.net/vtortola/P8JMm/3/
And the same demo working normally (without limiting the scope) here: http://jsfiddle.net/vtortola/P8JMm
The question is, how to indicate in the use of my directive that I want to use a particular property of my $scope as scope of my directive. It should be possible to bind the directive to arbitrary properties in the same $scope.
Cheers.
So the basic answer to this question is - you can do what you want to do, but it is a bit more complicated than you might think. To understand what is happening here you need to know about scopes in angular. A scope is essentially an object that contains the data accessible to the view. There are (at least) three ways scopes operate in angular:
Isolated - In this case angular basically creates a brand new scope for the directive. None of the properties are copied over.
Extended - In this case you would start with the root scope but make a shallow copy of it. Objects that are changed will be changed in the root scope but primitives will not be.
Shared - In this case you share share some or even all of the data with the root scope.
Based on your question above, what you what to do here is to extend the parent scope - copying an object to a property with a specific name in the newly created child scope. The way to get this behavior is to manually create a new child scope before the transclude. The two key lines of code to do this are:
// create a "new" scope
var childScope = scope.$new();
// extend using the model binding provided
angular.extend(childScope, scope[iAttr.myModel]);
In the context of your directive this looks like:
.directive('myDirectiveWrapper', ['$compile', function ($compile) {
return {
transclude: true,
restrict: 'E',
compile: function (element, attrs, transclude) {
var contents = element.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
var childScope = scope.$new();
angular.extend(childScope, scope[iAttr.myModel]);
if (!compiledContents) {
compiledContents = $compile(contents, transclude);
}
compiledContents(childScope, function(clone, childScope) {
iElement.append(clone);
});
};
},
template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
}])
Now you can specify any variable that you want as the "model" for the child scope and you can then access that directly in the contents of your transcluded code!
SEE THE FIDDLE: http://jsfiddle.net/P8JMm/7/
EDIT: Just for fun, I created a more complicated use case for this directive: http://jsfiddle.net/P8JMm/9/
Note - angular site also has some really good resources to understand scope better. See here.
If you want two way binding to work it's going to be a lot easier to just create a variable on your directive scope rather than apply mymodel directly onto the directive scope.
HTML
<div ng-app="myapp">
<div ng-controller="myController">
<my-directive-wrapper model="mymodel">
<my-directive-inner ng-repeat="item in mymodel.items" />
</my-directive-wrapper>
</div>
</div>
Directive
.directive("myDirectiveWrapper", function(){
return {
scope: {
model: '='
},
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, controller) {
},
template: "<div><h3>{{ model.name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
})
http://jsfiddle.net/kQ4TV/
If you don't care about two way binding I suppose you could do something like this but I wouldn't recommend it:
.directive("myDirectiveWrapper", function(){
return {
scope: {
model: '='
},
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, controller) {
angular.extend(scope, scope.model);
},
template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
})
http://jsfiddle.net/vWftR/
Here is an example of when that second approach can cause problems. Notice that when you enter something into the input field it will change the directive's name but not the name in the outer scope: http://jsfiddle.net/r5JeJ/

Call function on directive parent scope with directive scope argument

I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk

Categories