AngularJS directive with callback in scope prevents other directives from working - angularjs

I've created a directive with an isolated scope:
directive("myDirective",function() {
return {
restrict: "A",
scope: {aaa: '='},
link: function(scope, elem) { }
};
})
The directive works well, but it prevents all other dierctives on the same element from working...
I've reproduced the problem on this jsfiddle: http://jsfiddle.net/ironshay/k5ndr/
I'm using angular 1.0.7.
Am I doing something wrong? or is it some kind of an angular bug?

The issue is that you've created a child scope by using scope: {} on the directive. So the foo variable isn't on that scope anymore, it's on the parent. By using $parent.foo everything works correctly:
<input type="text" placeholder="with directive" ng-disabled="!$parent.foo || $parent.foo == ''" my-directive func="myFunc()" />
jsfiddle: http://jsfiddle.net/k5ndr/2/

You are creating isolated scope, which i think is causing the other directives also to get the isolated scope.
You can try to pass the foo variable to the isolated scope as you did the myFunc function.
<input type="text" placeholder="with directive" foo='foo' ng-disabled="!foo || foo == ''" my-directive func="myFunc()" />
I tried to create a fiddle for the same
http://jsfiddle.net/njXjj/

Related

Access to template scope from isolated scope

I'm trying to implement an AngularJS directive which has it own isolated scope in order to make it reusable in the same page. This directive is described by a template which is in another file so I use the templateUrl option.
app.directive('inputSettings', function () {
return {
restrict: 'E',
transclude: true,
templateUrl: 'templates/admin/inputSettingsTemplate.html',
controller: 'InputSettingsCtrl',
scope: {
settingkey: '=',
value: '=',
range: '=',
check: '=',
autosave: '='
}
};
});
In this template, I have a form named form.
<form name="form" class="settingForm" novalidate>
<div class="form-group" ng-class="{ 'has-error' : form.input.$invalid, 'has-success' : form.input.$valid }">
<input class="form-control" name="input" type="text" ng-model="valueInput" />
<p ng-show="form.input.$invalid && !form.input.$pristine" class="help-block" translate="{{ 'inputSettingErrorMessage' | translate:translationData }}"></p>
</div>
</form>
The problem I have is that I want to be able to set the validity of the input inside the template using a code like the following in the controller of my directive :
$scope.form.input.$setValidity("input", true);
But I have an error when I try to execute such a code. It seems that $scope does not know the form so I can't interact with my form within the controller of my directive.
Have you any idea? Is there anything I did wrong?
Thanks in advance !
Actually I manage to make my code works with a minimum amount of changes. I don't know if it is the good answer but it works like a charm for me.
I simply replaced the <form> tag by the <ng-form> and now it works perfectly. It means that the controller of my directive now can access to the form by it name.
Generally in a directive , to set the validity of a element we require ngModel for the directive and then we try to set the $validity of the element using the ngModelController
You have your controller for that directory, you can use that controller in order to manipulate the data.
function InputSettingsCtrl(){
var vm = this;
console.log(vm);
}
Also remember to add the controllerAs to your directive
controllerAs: 'ctrl',
Like this you can bind the variable from the controller to the view
Use $rootScope. It`s global. It contains all $scopeS..
Like
$rootScope.data = data; $scope.data = $rootScope.data

Use ng-model with a primitive value inside ng-transclude

In a legacy project, I want to create a new directive that uses transclude.
A trimmed down version of the directive code is:
app.directive('controlWrap', function() {
return {
restrict: 'E',
transclude: true,
scope: { label: "#" },
templateUrl: "control-wrap-template.html"
}
})
And the template is:
<div>
<label>{{label}}</label>
<div>
<ng-transclude></ng-transclude>
</div>
</div>
This directive is used like this
<control-wrap label="Just a example">
<input type="text" ng-model="input" />
</control-wrap>
Test: {{input}}
I know that the workaround is to use a object in the scope instead of primitive value (ng-model inside ng-transclude). But that is no option for me. It is a ugly, poorly coded, legacy code that relies in those attributes directly on the scope.
Is there a something I can do in the directive to make that html works without change?
You can manually transclude (instead of using ng-transclude) and apply whatever scope (which is, in your case, scope.$parent) you need to the transcluded content:
transclude: true,
scope: { label: "#" },
template: '<div>\
<label>{{label}}</label>\
<placeholder></placeholder>\
</div>',
link: function(scope, element, attrs, ctrls, transclude){
transclude(scope.$parent, function(clone){
element.find("placeholder").replaceWith(clone);
});
}
Demo
The cleanest solution is to do some refactoring and passing an object instead of a primitive value, but if for some reason you cannot do that, you're not out of the options.
However, I wouldn't recommend any of these options
1) Bind input from the parent scope, that prevents creating a new value on the child scope upon write - butt keep in mind that accessing the parent scope hurts reusability of your directive.
Angular 1.2:
<input type="text" ng-model="$parent.input" />
Angular 1.3:
<input type="text" ng-model="$parent.$parent.input" />
(The difference is because the parent of the transcluded scope is the directive scope from 1.3)
2) Create some kind of wrapper object and pass that instead of the primitive value
$scope.inputWrapper = {};
Object.defineProperty($scope.inputWrapper, 'input', {
get: function() { return $scope.input },
set: function(newValue) { $scope.input = newValue; }
})
and pass this to the directive. But again, I would do some refactoring instead.

Why is this code broken in AngularJS 1.2?

Goodevening,
The following code seems to work in older versions of Angular, but not in the 1.2 version. I am not sure why that is. What happens in Angular 1.2, is that the $watch directive does not get hit. It does not execute. It does execute in Angular 1.0.1.
Any idea why?
HTML
<fieldset validatedmarker>
<legend>User</legend>
<input type="text" name="name" class="form-control" ng-model="name" required>
</fieldset>
And the JS.
var APP = angular.module('myApp', []);
APP.directive('validatedmarker', function() {
return {
restrict: 'AE',
scope: { },
link: function(scope,e,a) {
scope.$watch(function() {
return scope.name;
}, function(newValue, oldValue) {
console.log("change detected: " + newValue)
});
}
};
});
Working example (Angular 1.0.1)
http://jsfiddle.net/2w8xW/
Non working example (Angular 1.2.0)
http://jsfiddle.net/24P86/
Thanks!
The most probable reason for this is the treatment of isolated scopes, that has changed in AngularJS 1.2.0
In earlier version everything inside the directive element got the isolated scope, but now the elements inside the directive get the original parent scope. So when you are setting the value of scope.name you are setting on parent scope. The watch is on child scope.
See this SO answer AngularJS Scope differences between 1.0.x and 1.2.x and the changelog.
To make your example work you would have to create a template for your directive and set transclude: true in directive definition.
the main reason why this is hapening is probably due to the fact that you are creating an isolated scope over an already liked html since you are not providing a template but rather using the existing html wich is most likely to be linked to the parent scope so you should take your directives innerhtml and use it as a template for your directive
like
APP.directive('validatedmarker', [function() {
return {
restrict: 'AE',
scope: { },
template:'<fieldset><legend>User</legend><input type="text" name="name" class="form-control" ng-model="name" required="true"/>',
replace:true,
link: function(scope,e,a) {
scope.$watch(function() {
return scope.name;
}, function(newValue, oldValue) {
console.log("change detected: " + newValue)
});
}
};
}]);
<div validatedmarker></div>
you can see it here
http://jsfiddle.net/24P86/1/
You are specifying an isolated scope, but with no attributes inside. You can either add attributes inside the scope property of your directive, or remove it altogether if you don't need to isolate your directive's scope.
More info on the attributes can be found here (search on 'isolate' scope):
https://docs.angularjs.org/api/ng/service/$compile

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.

How can I change the scope of my element from an attribute directive?

<input type='text' ng-model='foo' my-dir='customFunction' />
{{foo}}
.directive('myDir',function(){
scope:{ customFunc : "&myDir"},
});
Now the scope will be overridden with myDir and foo won't be updated on the screen. But still each control that has my-dir attribute should have customFunction in an isolated scope.
Is it possible?
As mentioned in the comments above, one directive probably won't work everywhere. If the directive will be used with other directives like ng-model, ng-repeat, etc., an isolate scope probably won't work. Here's an example of a directive that uses $eval, and does not create a new scope:
<div ng-controller="MyCtrl">
<input type='text' ng-model='foo' my-dir='customFunction'>
<br>foo={{foo}}
</div>
app.directive('myDir', function() {
return {
link: function(scope, element, attrs) {
scope.$eval(attrs.myDir);
},
}
});
function MyCtrl($scope) {
$scope.customFunction = alert('hi');
$scope.foo = '22';
}
Fiddle
See also When writing a directive in AngularJS, how do I decide if I need no new scope, a new child scope, or a new isolated scope?

Resources