AngularJS, binding to a model in ng-repeat in a directive - javascript

So I am trying to bind inside a directive (to access outside) to a model inside of an ng-repeat. So in the outer controller I have a variable I would like to bind in like
//in the directive scope
filterArray: '='
Inside the directive that would be bound inside the directive to a model in an ng-repeat like so -
//inside the directive
<li ng-repeat="value in filter.values">
<input type="checkbox" ng-model="filterObject[filter.name][value]" ng-change="filterChange()">{{value}}
</li>
This worked fine until I changed the directive to have an isolate scope, now it is saying cannot set property of undefined. Is there any way to get this working as intended? The idea is the variable would build out when the user clicks the inputs so the outer controller would be able to see the built object.
Apologies if this is a bit confusing - I have made a fiddle to clarify : https://jsfiddle.net/vt1uasw7/42/ .
I want the outer controller to have access to the object built by binding the model - again this was working before I added the isolate scope. Thanks!
Edit: maybe the trick in this case is not to use the isolate scope? This one has me stumped, I've tried every combination of scope attributes :(.

If you can't pre-initialize your outter filterObject you can let the directive controller handle that for you:
$scope.filterArray[$scope.filter.name] = {};
And check your parameters as Claies stated, inside your directive you need to use filterArray and also the attribute name in the outter ng-repeat needs to be "filter-array and not "filterArray".
<div ng-repeat="filter in searchResults.filters" class="my-directive" filter="filter" filter-change="filterChange" filter-array="filterObject"> </div>
See this https://jsfiddle.net/vt1uasw7/164/

Related

Angular ng-model not binding to textarea [duplicate]

Here is my plnkr: http://plnkr.co/edit/n8cRXwIpHJw3jUpL8PX5?p=preview You have to click on a li element and the form will appear. Enter a random string and hit 'add notice'. Instead of the textarea text you will get undefined.
Markup:
<ul>
<li ng-repeat="ticket in tickets" ng-click="select(ticket)">
{{ ticket.text }}
</li>
</ul>
<div ui-if="selectedTicket != null">
<form ng-submit="createNotice(selectedTicket)">
<textarea ng-model="noticeText"></textarea>
<button type="submit">add notice</button>
</form>
</div>
JS part:
$scope.createNotice = function(ticket){
alert($scope.noticeText);
}
returns 'undefined'. I noticed that this does not work when using ui-if of angular-ui. Any ideas why this does not work? How to fix it?
Your problem lies in the ui-if part. Angular-ui creates a new scope for anything within that directive so in order to access the parent scope, you must do something like this:
<textarea ng-model="$parent.noticeText"></textarea>
Instead of
<textarea ng-model="noticeText"></textarea>
This issue happened to me while not using the ng-if directive on elements surrounding the textarea element. While the solution of Mathew is correct, the reason seems to be another. Searching for that issue points to this post, so I decided to share this.
If you look at the AngularJS documentation here https://docs.angularjs.org/api/ng/directive/textarea , you can see that Angular adds its own directive called <textarea> that "overrides" the default HTML textarea element. This is the new scope that causes the whole mess.
If you have a variable like
$scope.myText = 'Dummy text';
in your controller and bind that to the textarea element like this
<textarea ng-model="myText"></textarea>
AngularJS will look for that variable in the scope of the directive. It is not there and thus he walks down to $parent. The variable is present there and the text is inserted into the textarea. When changing the text in the textarea, Angular does NOT change the parent's variable. Instead it creates a new variable in the directive's scope and thus the original variable is not updated. If you bind the textarea to the parent's variable, as suggested by Mathew, Angular will always bind to the correct variable and the issue is gone.
<textarea ng-model="$parent.myText"></textarea>
Hope this will clear things up for other people coming to this question and and think "WTF, I am not using ng-if or any other directive in my case!" like I did when I first landed here ;)
Update: Use controller-as syntax
Wanted to add this long before but didn't find time to do it. This is the modern style of building controllers and should be used instead of the $parent stuff above. Read on to find out how and why.
Since AngularJS 1.2 there is the ability to reference the controller object directly instead of using the $scope object. This may be achieved by using this syntax in HTML markup:
<div ng-controller="MyController as myc"> [...] </div>
Popular routing modules (i.e. UI Router) provide similar properties for their states. For UI Router you use the following in your state definition:
[...]
controller: "MyController",
controllerAs: "myc",
[...]
This helps us to circumvent the problem with nested or incorrectly addressed scopes. The above example would be constructed this way. First the JavaScript part. Straight forward, you simple do not use the $scope reference to set your text, just use this to attach the property directly to the controller object.
angular.module('myApp').controller('MyController', function () {
this.myText = 'Dummy text';
});
The markup for the textarea with controller-as syntax would look like this:
<textarea ng-model="myc.myText"></textarea>
This is the most efficient way to do things like this today, because it solves the problem with nested scopes making us count how many layers deep we are at a certain point. Using multiple nested directives inside elements with an ng-controller directive could have lead to something like this when using the old way of referencing scopes. And no one really wants to do that all day!
<textarea ng-model="$parent.$parent.$parent.$parent.myText"></textarea>
Bind the textarea to a scope variable's property rather than directly to a scope variable:
controller:
$scope.notice = {text: ""}
template:
<textarea ng-model="notice.text"></textarea>
It is, indeed, ui-if that creates the problem. Angular if directives destroy and recreate portions of the dom tree based on the expression. This is was creates the new scope and not the textarea directive as marandus suggested.
Here's a post on the differences between ngIf and ngShow that describes this well—what is the difference between ng-if and ng-show/ng-hide.

AngularJs Avoiding Scoping Issues

I recently spent over 4 hours before figuring out why my ng-model directive used in combination with ng-options was not correctly binding to the property within my controller. The <select> element was being properly initialized - receiving a value from the controller (parent) scope. But the child scope was not correctly updating the parent scope. After checking out the following questions and plunkers, I was able to develop a "work around" for this issue:
Helpful stackoverflow question 1
Helpful stackoverflow question 2
Basic Plunker
I found that the property I was binding to in my <select> element was binding to a property of the same name within a child scope of the controller - therefore not the value was not reflected as expected in the controller's scope. After changing
<select ng-options="asset as asset.Name for asset in allAssets" ng-model="selectedAsset" ng-change="lookupAssetPermissions()"></select>
to
<select ng-options="asset as asset.Name for asset in allAssets" ng-model="$parent.selectedAsset" ng-change="lookupAssetPermissions()"></select>
The value in selectedAsset was correctly binding to the property in the controller's scope (as seen in the ng-change event handler). The entire context of my element is the following:
<!---outer div has controller level scope----->
<div>
<!---inner div creates child scope with ng-if----->
<div ng-if="true condition here">
<!---select statement from above----->
<select ng-model="$parent.selectedAsset">...</select>
</div>
</div>
Do I have any other options in this scenario other than purposefully binding to the parent scope? If I had multiple child scopes (nested ng-if statements), would I need to alter the ng-model to bind to $parent.$parent.$parent....selectedAsset in order to update the value in my controllers scope? Are there any "best practices" on this topic?
Put all variables inside some object i.e.:
$scope.Model = {
selectedAsset : 'mySelectedAsset1',
selectedAsset2 : 'mySelectedAsset2',
selectedAsset3 : 'mySelectedAsset3'
}
Then you can:
<div ng-repeat> //new scope
<div ng-repeat> // new scope
<input ng-model="Model.selectedAsset">
This also lows your 'dependency' on $scope, defining such Model object will show everyone who is reading your code what model u have.

AngularJS does not refresh ng-show when using only attributes

I have an AngularJS app, and just noticed that if I use ng-show and ng-click together and don't use function on the controller, then the ng-show is not working as expected.
So I have this:
<div ng-app ng-controller="Controller">
<div ng-repeat="d in data">
<button ng-show="showEdit!==d" ng-click="showEdit=d">{{d}}</button>
</div>
</div>
With a controller:
function Controller($scope){
$scope.data=[1,2,3]
}
Fiddle: http://jsfiddle.net/sashee/v6XrK/
And the buttons are disappearing when I click them, and never reappear.
If I change the view to this:
<div ng-app ng-controller="Controller">
<div ng-repeat="d in data">
<button ng-show="showEdit!==d" ng-click="show(d)">{{d}}</button>
</div>
</div>
And the controller:
function Controller($scope){
$scope.data=[1,2,3]
$scope.show=function(d){
$scope.showEdit=d;
}
}
Fiddle: http://jsfiddle.net/sashee/UFGv4/
Everything works as expected. I think both versions should do exactly the same, but they aren't. What could be the difference or the explanation?
Simple, ng-repeat creates it's own scope, so when you do showEdit=d inside your ng-repeat -- showEdit is limited to the scope of the current repeater. Your variable gets set and your button disappears.
In the example where you call the function, you have a variable $scope.showEdit -- well that variable isn't limited to the repeat scope, so you will always have two buttons showing since showEdit is being assigned differently due to each click.
Please use spaces in your angular expressions.
The difference is that your ng-show and ng-click are in an ng-repeat. Each ng-repeat element has its own scope. So when you do the following:
ng-click="showEdit=d"
You are creating a new value of showEdit on the anonymous ng-repeat scope, not the scope that your controller knows about. Other ng-repeat elements know nothing about it, so your conditional ng-show="showEdit!==d" reflects the value that you just set, not the value of showEdit from your controller.
Well, it's a bit quirky.
ng-repeat creates a new scope for each iterated element.
When you bind the click expression showEdit=d, you actually bind this expression to the Nth scope, created by the ng-repeat (hence the showEdit is located in scopeN).
When you simply use the function from the Controller, you change the showEdit field of the Controller's scope!
- Controller scope (showEdit in the 2nd example)
- scope 1 (showEdit 1 in the 1st example)
- scope 2 (showEdit 2 in the 1st example)
...
- scope N (showEdit N in the 1st example)

AngularJS: Directive not getting controller's variable from scope

I have an element that has both a controller and a directive with an isolate scope applied to it:
scope: {
dirVar: '='
}
The goal is to run certain parts of the directive only if a variable holds true. I'm setting that variable in the controller and trying to pass it into the directive through an attr.
The problem is that when I do something like
<div ng-controller="MyCtrl" my-directive active="ctrlVar"></div>
and try to get active in the directive with scope.active, it always comes up undefined.
Here is an example: http://jsfiddle.net/u3t2u/1/
Any explanation as to why or how to properly do this? I assume the problem is with the controller and directive being applied to the same element and wish to get around that.
Another option would be to remove the directive's isolate scope and have it evaluate an attr passed to it, but I'm not sure how to do that ($parse keeps throwing errors).
That is because your directive is not inside the controller. Try this:
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<div my-directive="" active="myValue">
Testing.
</div>
</div>
</div>
Ended up changing the way I structured the directive because it wasn't something that should really have had an isolate scope, and the only reason it did was so it could take expressions and evaluate them to true or false.
So I changed it to use $parse, which left the directive looking something like:
var active = $parse(attrs.isActive);
// Evaluate contents of attrs.isActive
// as if they are variables within its scope,
// which is inherited from parent scopes
if(active(scope)) {
// do something
}
I am not too familiar with certain things like transclude and creating an isolated scope, but this is what I got after reading the docs for Directives and fiddling around:
http://jsfiddle.net/u3t2u/4/
I only changed this portion of the html:
<div ng-controller="MyCtrl">
<div my-directive active="myValue">
Testing.
</div>
</div>
I believe that in this case, you do not actually have to pass a value to the my-directive directive, since you are already using an isolate scope with an =. Sorry if my explanation is not that good. You can read more at http://docs.angularjs.org/guide/directive , under the section Writing directives (long version).

Access directive's isolate scope from within transcluded content

I'm not sure if this is actually possible, but I'm essentially wanting a reverse of the '&' isolate scope in AngularJS. Here is a Plunkr to demonstrate.
Basically, I have a custom directive set up that delivers some reusable HTML. It makes use of ng-transclude to allow some external content to be rendered within it. However, I have found a situation where I would like to access a function that has been set up on the isolate scope for the directive, from within the transcluded section of code.
So essentially I have something that looks like:
<div ng-controller="MainController">
<!-- The directive -->
<div some-custom-directive>
<!-- Button 1 that invokes a method within the controller scope -->
<button id="button1" ng-click="invoke1()">Invoke from Controller</button>
<!-- Button 2 that invokes a method on the isolate scope for the custom directive -->
<button id="button2" ng-click="invoke2()">Invoke from Isolate Scope</button>
</div>
</div>
Does anyone know if this is indeed possible?
Update
As per #Mark Rajcok's answer, the $$prevSibling found on the $scope can be used to access the isolate scope of the directive from within the transcluded content. However, I have updated the above Plunkr to attempt this from within an ng-repeat directive, which does not work. I am assuming the items within that repeat do not inherit the scope.
Although possible, the solution I present is a hack, as it uses a scope internal variable, $$prevSibling.
Inside your transcluded content, you are inside the transcluded scope. The directive's isolate and transcluded scopes are siblings. To get from the transcluded scope to the isolate scope, you can use $$prevSibling. (To get from the isolate scope to the transcluded scope, you can use $$nextSibling.)
So, this hack will work:
Invoke the Directive Action
To call a method on the controller scope, you need to specify that using & as #ganaraj already demonstrated:
<content-presenter controller-action="action()">
Then in your directive:
scope: { controllerAction: '&' },
template: ... +
'<button ng-click="controllerAction()">Trigger Ctrl action()</button>' ...
Plunker
With ng-repeat (see Samuel's comment), each item creates a child scope of the transcluded scope. In the picture below, I only show one item to keep the picture smaller. Reverse the $$nextSibling brown arrow for $$prevSibling.
So the hack to get to method action() defined on the isolate scope from an ng-repeat child scope is
<div ng-repeat="item in items" ng-click="$parent.$$prevSibling.action()">
Update for Angular v1.3+:
Since Angular v1.3, the transcluded scope is now a child of the directive's isolate scope – they are no longer siblings. So to get from the transcluded scope to the isolate scope, use $parent.
With ng-repeat, each item still creates a child scope of the transcluded scope:
But to get to method action() defined on the isolate scope from an ng-repeat child scope, we now use
<div ng-repeat="item in items" ng-click="$parent.$parent.action()">
Here is a plunk that solves your current problem. I am not sure what you are attempting to do. But as far as I know, there is no way of calling something in the isolate scope from an external scope. Ofcourse, you could setup a two way bound variable between the isolate scope and the external scope, change the variable in the external scope and $watch for it on the isolate scope ( this will work like an eventing mechanism )... That is one way of doing what you are attempting to do.. if you insist on it.
Alternatively, there is a mechanism to call a function on the external scope from the isolate scope. Its kind of like a callback.
See this http://plnkr.co/edit/5MT4vo9qXtV6nQikfEiH?p=preview
It is my understanding that adding ng-repeat to an element creates a new scope, in order for the repeating content to be correctly bound. You may need an additional $parent in that chain, such as $parent.$$nextSibling, in order to step up to the level that is adjacent to the directive's isolate scope.

Categories

Resources