Update a scope variable from a directive (angularJS) - angularjs

I have a directive I would like to apply to multiple input elements to change their value. I've been successful in applying it to the input elements value, but for some reason that is not being reflected in the scope. I'm kinda new to Angular and apologize if I'm missing some kind of obvious answer.
http://jsfiddle.net/hmko75td/ JS Fiddle
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<select ng-model='Factor'>
<option value=1>1</option>
<option value=2>2</option>
<option value=5>5</option>
</select>
<br />
<input convert-input ng-model="myNumber">
{{myNumber}}
<br />
<input convert-input ng-model="myNumber2">
{{myNumber2}}
<br />
<input convert-input ng-model="myNumber3">
{{myNumber3}}
<br />
</div>
</div>
var app = angular.module('myApp',[]);
app.controller('MyCtrl', function($scope) {
$scope.myNumber = 1;
$scope.myNumber2 = 2;
$scope.myNumber3 = 3;
$scope.Factor = 1;
});
app.directive("convertInput", function () {
return {
require: 'ngModel',
restrict: "A",
link: function (scope, element, attrs) {
scope.$watch('Factor', function () {
if(scope.Factor){
element[0].value = scope.Factor * element[0].value;
}
});
}
};
});
This simplified example shows the crux of my problem. When changing the value of the droplist it correctly updates the element's value on the page, but that does not get translated correctly back into the scope variable.
Any ideas how to either 1) tell the directive which scope variable needs to be updated or 2) force the model to update based on the inputs value?
Thanks!

One constraint I see with your snippet is that you need all uses of your convert-input directive to share Factor, which exists in an enclosing parent scope.
In such a case, one viable approach would be to use the attrs passed into the directive to extract the name of the ng-model binding, and then to update the corresponding binding via scope.
scope[attrs.ngModel] *= scope.Factor;
Here's a fork of your JSFiddle demonstrating this:
http://jsfiddle.net/m4hvre2y/
Another approach to having directives update an ng-model in a parent scope would be to declare two-way binding (e.g. scope: { ngModel: '='), but it isn't applicable in your case due to the constraint I mentioned above. If you did this, the isolated scope means you lose access to Factor unless it's specifically passed into the directive.

Related

Trouble with executing Angular directive method on parent

I have a directive inside a directive, and need to call a parent method using the child directive. I'm having a bit of trouble passing the data around, and thought y'all might have an idea.
Here's the setup:
My parent directive is called screener-item. My child directive is called option-item. Inside of every screener-item, there might be n option-items, so they're dynamically added. (Essentially, think of this as dynamically building a dropdown: the user gives it a title, then a set of options available)
Here's how this is set up:
screener-item.directive.js
angular.module('recruitingApp')
.directive('screenerItem', function(Study, $compile) {
return {
templateUrl: 'app/new-study/screener-item/screener-item.html',
scope: {
study: '='
},
link: function(scope, el, attrs) {
var options = [];
scope.addOptionItem = function(item) {
options.push(item);
}
scope.saveScreenerItem = function() {
if (scope.item._id) {
var isEdit = true;
}
Study.addScreenerQuestion({id:scope.study._id},{
_id: scope.item._id,
text: scope.item.text,
type: scope.item.type
}, function(item){
scope.mode = 'show';
scope.item._id = item._id;
if (!isEdit) {
el.parent().append($compile('<screener-item study="newStudy.study"></screener-item')(scope.$parent));
}
});
}
}
}
});
screener-item.html
<div class="screener-item row" ng-hide="mode == 'show'">
<div class="col-md-8">
<input type="text" placeholder="Field (e.g., name, email)" ng-model="item.text">
</div>
<div class="col-md-3">
<select ng-model="item.type">
<option value="text">Text</option>
<option value="single_choice">Single Select</option>
<option value="multi_choice">Multi Select</option>
</select>
<div ng-show="item.type == 'single_choice' || fieldType == 'multi_choice'">
<h6>Possible answers:</h6>
<option-item item-options="options" add-option-item="addOptionItem(value)"><option-item>
</div>
</div>
<div class="col-md-1">
<button ng-click="saveScreenerItem()">Save</button>
</div>
</div>
<div class="screener-item-show row" ng-model="item" ng-show="mode == 'show'">
<div class="col-md-8">{{item.text}}</div>
<div class="col-md-3">({{item.type}})</div>
<div class="col-md-1">
<a ng-click="mode = 'add'">edit</a>
</div>
</div>
You'll notice option-item which is included there in them middle. That's the initial option offered to the user. This may be repeated, as the user needs it to be.
option.item.directive.js
angular.module('recruitingApp')
.directive('optionItem', function($compile) {
return {
templateUrl: 'app/new-study/screener-item/option-item.html',
scope: {
addOptionItem: '&'
},
link: function(scope, el, attrs) {
scope.mode = 'add';
scope.addItem = function(value) {
console.log("Value is ", value);
scope.addOptionItem({item:value});
scope.mode = 'show';
var newOptionItem = $compile('<option-item add-option-item="addOptionItem"></option-item')(scope);
el.parent().append(newOptionItem);
}
}
}
});
option-item.html
<div ng-show="mode == 'add'">
<input type="text" ng-model="value">
<button ng-click="addItem(value)">Save</button>
</div>
Here's what I want to happen: When the user enters a value in the option-item textbox and saves it, I want to call addItem(), a method on the option-item directive. That method, then, would call the parent method - addOptionItem(), passing along the value, which gets pushed into an array that's kept on the parent (this array keeps track of all the options added).
I can get it to execute the parent method, but for the life of me, I can't get it to pass the values - it comes up as undefined each time.
I'm trying to call the option-item method instead of going straight to the parent, so that I can do validation if needed, and so I can dynamically add another option-item underneath the current one, once an item is added.
I hope this makes sense, please let me know if this is horribly unclear.
Thanks a ton!
EDIT: Here's a jsFiddle of it: http://jsfiddle.net/y4uzbapz/1/
Note that when you add options, the logged out array of options on the parent is undefined.
Got this working. All the tutorials have this working by calling the parent method on ng-click, essentially bypassing the child controller. But, if you need to do validation before passing the value up to the parent, you need to call a method on the child directive, then invoke the parent directive's method within that call.
Turns out, you can access it just the same way that you can as if you were putting the expression inside of ng-click.
Here's a fiddle showing this working: http://jsfiddle.net/y4uzbapz/3/
Notice that the ng-click handler is actually on the child directive, which calls the parent directive's method. This lets me do some pre/post processing on that data, which I couldn't do if I'd invoked the parent directive directly from ng-click.
Anyway, case closed :)

How to pass string and bool variables automatically to directive parent scope

I bind variable in a directive like this :
<path-filter-modal is-opened="filterModalIsOpened">
And in the directive I use '=' binding like this:
scope: {
isOpened: '='
}
When I change variable in directive, a parent scope contains own value.
How can I make that the parent scope contain the same value?
For objects it works well but not with strings and booleans.
Notice that I use controller that is defined in my directive in my directive to change values.
Because JavaScript is designed to be so.
Defining an isolate scope in the directive creates a new $scope object, which is a separate $scope object. Its only relationship with the parent scope is that: $isolateScope.$parent === $parentScope. It doesn't inherits from $parentScope prototypical.
When you assign some primitive type (string/boolean) to $scope.isOpened, actually JavaScript engine will create a new variable isOpened on $scope. It is totally not related to $parentScope.isOpened.
But now, Angular syncs the two variables for you implicitly. So binding primitive variables still makes two-way binding work well. Please check JSFiddle.
If you binds to some object type, the child scope and parent scope are referencing to the exactly the same copy of an object in the memory. Changing on the parent scope will change the child scope automatically. So two-way binding is always recommended to bind objects, not primitive types.
Check this JSFiddle. I bind a primitive and an object to the directive myDirective. Then modify them inside the link function:
scope.primitiveParam = 'primitive from directive';
// $parent.primitive and primitiveParam refer to different memory;
// Angular is responsible to sync them.
console.log(scope.$parent.primitive);
console.log(scope.primitiveParam);
scope.objectParam.name = 'object from directive';
// $parent.obj and objectParam refer to an identical object
console.log(scope.$parent.obj.name);
console.log(scope.objectParam.name);
console.log(scope.objectParam === scope.$parent.obj);
And the result is like:
primitive from parent
primitive from directive
object from directive
object from directive
For more details: Understanding Scopes (here are many intuitive images illustrating the concepts clearly)
RE: For objects it works well but not with strings and booleans
I think it's the usual case of prototypal inheritance problem. When the model come from object it works well, but if it come from non-objects there's a possibility that the ng-model is created on child scope.
To solve that problem, use modern approach, use Controller as approach. Or put the filterModelIsOpened in an object. The first approach is better.
<div ng-controller="SomeController as s">
<path-filter-modal is-opened="s.filterModalIsOpened">
</div>
function SomeController() { // no need to use $scope
this.filterModalIsOpened = false;
}
Or if you are using older version of Angular, you cannot use Controller as approach. Just create your own alias in the controller:
<div ng-controller="SomeController">
<path-filter-modal is-opened="s.filterModalIsOpened">
</div>
function SomeController($scope) {
$scope["s"] = this;
this.filterModalIsOpened = false;
}
Here's a good article explaining the prototypal inheritance: http://codetunnel.io/angularjs-controller-as-or-scope/
Here are the demo why you should always prefix your model, be they are object or primitive.
Not recommended. Live code demo: http://jsfiddle.net/hdks813z/1/
<div ng-app="App" ng-controller="Ctrl">
<div ng-if="true">
<input type="checkbox" ng-model="filterModalCanBeOpened"/>
<the-directive primitive-param="filterModalCanBeOpened"></the-directive>
</div>
<hr/>
<p>
The value below doesn't react to changes in primitive(non-object) property
that is created a copy on a directive(e.g., ng-repeat, ng-if) that creates
child scope
</p>
$scope.primitive: {{filterModalCanBeOpened}}
</div>
angular.module('App', [])
.directive('theDirective', function () {
return {
restrict: 'AE',
scope: {
primitiveParam: '='
},
template: '<div>primitiveParam from directive: {{ primitiveParam }}; </div>',
link: function (scope) {
}
};
})
.controller('Ctrl', ['$scope', function ($scope) {
$scope.filterModalCanBeOpened = true;
}]);
Recommended: Live code demo: http://jsfiddle.net/2rpv27kt/
<div ng-app="App" ng-controller="Ctrl as c">
<div ng-if="true">
<input type="checkbox" ng-model="c.filterModalCanBeOpened"/>
<the-directive primitive-param="c.filterModalCanBeOpened"></the-directive>
</div>
<hr/>
<p>
The value below react to changes in primitive(non-object) property that is
addressed directly by its alias c, creating child scope on it would be
impossible. So the primitive below react to changes on
the c's filterModalCanBeOpened.
</p>
c.primitive: {{c.filterModalCanBeOpened}}
</div>
angular.module('App', [])
.directive('theDirective', function () {
return {
restrict: 'AE',
scope: {
primitiveParam: '='
},
template: '<div>primitiveParam from directive: {{ primitiveParam }}; </div>',
link: function (scope) {
}
};
})
.controller('Ctrl', [function () {
this.filterModalCanBeOpened = true;
}]);

AngularJS : ng-if | Hidden(Removed) ng-model variable not removed from $scope

I am trying to understand the working of ng-if in contrast with ng-show.
After reading the docs and going through the related stackoverflow question here, I understand that ng-if removes the DOM element and the scope variables inside that ng-if are removed.i.e ng-model variables inside the 'removed' ng-if element wont appear in $scope.
From the Angular ng-if docs:-
Note that when an element is removed using ngIf its scope is destroyed
and a new scope is created when the element is restored. The scope
created within ngIf inherits from its parent scope using prototypal
inheritance. An important implication of this is if ngModel is used
within ngIf to bind to a javascript primitive defined in the parent
scope.
Consider following code snippet:-
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.10/angular.min.js"></script>
</head>
<body data-ng-app="myModule">
<div data-ng-controller="TestController">
<input name="first" type="number" data-ng-model="form.x" placeholder="Enter Number X"/>
<input name="second" type="number" data-ng-if="form.x>5" data-ng-model="form.y" placeholder="Enter Number Y"/>
<input type="button" value="Click" ng-click="save()"/>
</div>
<script type="text/javascript">
var myModule = angular.module("myModule",[]);
myModule.controller("TestController",function($scope){
$scope.form = {};
$scope.form.x = 0;
$scope.form.y = 0;
$scope.save = function(){
console.log($scope.form);
};
});
</script>
</html>
This is pretty simple use case - show the second number input field only when first is greater than 5.
The save button click delegates to 'save' function in the controller which simply prints out the 'form' object of $scope.
Problem:-
Input 1:-
Enter x=6 and y=2
Output 1 : {x: 6, y: 2}
Input 2:-
Enter x=3
Output 2 : {x: 3, y: 2}
I am not able to understand why 'output 2' still shows y =2. If its DOM has been removed, shouldn't the output be just {x:3} ?
What should I do if I want to remove (ngIf-removed) model from the scope?
Thanks
Problem
To further what #Chandermani pointed out in comments, ng-if creates a new scope, which has its own variables. It does, however, prototypically inherit from its parent scope, so if you set a property on an existing parent object, such as what you're doing by using form.y, when the child scope is destroyed, that property remains unaffected.
Quick fix solution
You could add another directive to the same element as the one you're setting ng-if on, which deletes the property from the scope on $destroy:
Directive
myModule.directive('destroyY', function(){
return function(scope, elem, attrs) {
scope.$on('$destroy', function(){
if(scope.form.y) delete scope.form.y;
})
}
});
View
<input ... data-ng-if="form.x>5" destroy-y .../>
Demo
Note: I saw that #user2334204 posted something similar. I decided to post this anyway because here the value of x won't have to be checked every digest
Hi there :D you could set a watcher on your "x" variable, something like this:
$scope.$watch(function(){
return $scope.form.x;
},function(){
if($scope.form.x < 5) delete $scope.form.y;
});
Although i don't know if using "delete" is a good practice...
Hope it works for you.
----EDIT----
Another approach:
<input ng-model="form.x" ng-change="check(form.x)">
And in your controller:
$scope.check = function(x){
if(x < 5 ) delete $scope.form.y;
};
Though i think #Marc Kline option is even better.
For dynamic key, you can define a directive like below:
myModule.directive('removeKey', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$on('$destroy', function () {
let attributes = scope.$eval(attrs.removeKey);
if (scope.$parent[attributes.mainModel].hasOwnProperty(attributes.modelKey))
delete scope.$parent[attributes.mainModel][attributes.modelKey];
});
}
};
});
and your view something looks like this:
<div ng-if="condition === 0">
<input ng-model="myFormJson.inputOne" remove-key='{"mainModel":"myFormJson","modelKey":"inputOne"}' />
</div>
<div ng-if="condition === 1">
<input ng-model="myFormJson.inputTwo" remove-key='{"mainModel":"myFormJson","modelKey":"inputTwo"}' />
</div>

Angular Directive's template binding doesn't update

I have a directive set up here http://jsfiddle.net/screenm0nkey/8Cw4z/3 which has two bindings to the same scope property, but for some reason the binding in the directive's template property doesn't update when the model changes (after typing in the input).
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count">
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
}
};
});
But if I move the input position in the markup it works and both bindings update.
<input type="text" ng-model="count">
<test>
<h3>Inner {{count}}</h3>
</test>
http://jsfiddle.net/screenm0nkey/dCvZk/3
Can anyone explain why the position of the input containing the binding, would have an affect the bindings. I assumed that during the digest loop the watchers for both binding would be updated regardless of the position of the markup.
Many thanks
To me, this seems purely to be a scope issue. Lets take a look at the markup that is generated by both:
Not working:
<body ng-app="App" class="ng-scope">
<h1 class="ng-binding">Outer1 <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner 1</h3>
<input type="text" ng-model="count" class="ng-scope ng-pristine ng-valid">
</div>
</h1>
</body>
Working:
<body ng-app="App" class="ng-scope">
<input type="text" ng-model="count" class="ng-valid ng-dirty">
<h1 class="ng-binding">Outer <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner </h3>
</div>
</h1>
</body>
The ng-scope class is a useful marker for where Angular is declaring a new scope.
You can see by the markup that the in the working example both the count properties are enclosed in the scope that is attached to body. So, in this case, the directive scope is a child of the body scope (and therefore has access to it).
However, In the example that is not working, the Outer1 property is sitting outside of the scope that the input is in.
The Angular Scope documentation covers this well. The scopes are arranged in a hierarchy with child scopes having access to parent scopes (but not the other way around):
The application can have multiple scopes, because some directives
create new child scopes (refer to directive documentation to see which
directives create new scopes). When new scopes are created, they are
added as children of their parent scope. This creates a tree structure
which parallels the DOM where they're attached
Long story short - as others have said, this is a scope issue. Using the "ng-transclude" directive creates a new scope. When a new scope is created values from the old scope will be accessible in the new scope (hence the first replace) but after that only objects that are shared between the old/new scope will be updated. That is why using an object would work, but using a value will not.
In your case placing the input field inside of the ng-transclude causes this to only edit the value in that scope, not the value in the parent scope (which is where the count for the "test" directive is pulled from).
Incidentally, this can be an issue with repeaters (ng-repeat) as well as other directives. Its best to use a tool such as "Batarang" in order to find issues such as this. It allows you to look at what is in each scope and determine why the screen isn't showing the "correct" data. Hope that helps explain further!
Add ng-change to input , it should work. The problem is that controller into directive doesn't know about count change.
JS
var App = angular.module('App', []);
App.directive('test', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer {{this.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
$scope.onChange = function(count){
$scope.count = count;
}
}
};
});
HTML
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count" ng-change="onChange(count)">
</test>
Demo Fiddle
The order matters because of the difference between creating a property on the scope versus actually using an object bound to the scope (especially when a transclude creates a new child scopr). Best practice is to use an object on the scope and bind properties to that object when scope issues can come into play with directives and transcludes.
If you change your code to this, it will work as you were expecting and order does not matter. Notice that I am creating a scope object and placing the count as a property on that object.
<test>
<h3>Inner {{data.count}}</h3>
<input type="text" ng-model="data.count"/>
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{data.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.data = {};
$scope.data.count = 1;
}
};
});
This is a great tutorial on this subject. Props to EggHead. https://egghead.io/lessons/angularjs-the-dot
It's a scoping issue.
$scope.count = 1; adds the property count to the scope that <test> is in. Let's call it parent scope.
ng-transclude creates a new scope, let's call it child scope. When <h3>Inner {{count}}</h3> is evaluated, the child scope has no property count so it's read from the parent scope.
<input type="text" ng-model="count"> binds the value of the input to the property count in the child scope. As soon as you enter something the property will be created if it's not there yet. From this point on <h3>Inner {{count}}</h3> gets its value from the child scope.
Scopes in angular are simple JavaScript objects and are connected to their parents via prototypes. So before you enter something the child scope looks something like
{
prototype: { // = parent scope
count: 1
}
}
When you change the value to, say, 5, the scope looks something like
{
count: 5,
prototype: { // = parent scope
count: 1
}
}
Because data binding does something like scope.count = 5.
Here's a work around
Change $scope.count to
$scope.helper = {
count: 1
}
and refactor the rest.
Check this video out for an explanation.
It seems that we cannot override this since ngTransclude will use $transclude function directly.
See: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngTransclude.js
and: http://docs.angularjs.org/api/ng.$compile
transcludeFn - A transclude linking function pre-bound to the correct transclusion scope. The scope can be overridden by an optional first argument. This is the same as the $transclude parameter of directive controllers. function([scope], cloneLinkingFn).

Angular directives: mixing attribute data and ngModel

I've had luck building directives that share scope, and ones that isolate scope, but I'm having trouble figuring out the correct way to do both.
<input type="text" ng-model="search" append-me terms="myTerms">
I'm trying to take an input like the one above, and staple a UL that ng-repeats over a list of attributes after it. I have two problems.
1) How do I correctly interface the shared ng-model scope?
2) What am I doing incorrectly with this compile function?
http://jsfiddle.net/vEQ6W/1/
Mixing isolated scope with ngModel is a documented issue, see the Isolated Scope Pitfall section in the documentation:
Isolated Scope Pitfall
Note that if you have a directive with an isolated scope, you cannot require ngModel since the model value will be looked up on the isolated scope rather than the outer scope. When the directive updates the model value, calling ngModel.$setViewValue() the property on the outer scope will not be updated. However you can get around this by using $parent.
Using this knowledge and some freaky scope experiments I've come with two options that does what you want to do:
(1) See this fiddle It makes use of the $parent method as described above.
<div ng-controller="MyCtrl">
<div>
<input ng-form type="text" ng-model="$parent.search" append-me terms="myTerms">
</div>
{{search}}
</div>
myApp.directive('appendMe', ['$compile', function($compile) {
return {
restrict: 'A',
scope: {terms: '='},
link: function(scope, element, attributes) { // linking function
console.log(scope.terms);
var template = '<p>test</p>' +
'<ul><li ng-repeat="term in terms">{{term}}</li></ul>' +
'<p>hm…</p>'
element.after($compile(template)(scope));
}
}
}]);
(2) See this fiddle It does not use $parent, instead it uses the isolated scope to publish the search model as configured via ngModel.
<div ng-controller="MyCtrl">
<div>
<input ng-form type="text" ng-model="search" append-me terms="myTerms">
</div>
{{search}}
</div>
myApp.directive('appendMe', ['$compile', function($compile) {
return {
restrict: 'A',
scope: {terms: '=', search: '=ngModel'},
link: function(scope, element, attributes) { // linking function
console.log(scope.terms);
var template = '<p>test</p>' +
'<ul><li ng-repeat="term in terms">{{term}}</li></ul>' +
'<p>hm…</p>'
element.after($compile(template)(scope));
}
}
}]);
Both options seem to work just fine.
Regarding #2, "What am I doing incorrectly with this compile function?"
If you change your compile's code snippet from...
...
tElement.after(
'<p>test</p>' +
'<ul><li ng-repeat="term in terms">{{term}}</li></ul>' +
'<p>hm…</p>'
);
...
to...
...
tElement.after(
'<p>test</p>' +
'<ul><li ng-repeat="term in myTerms">{{term}}</li></ul>' +
'<p>hm…</p>'
);
...
the ng-repeat will render correctly. I cannot, however, tell you WHY it works.

Resources