Angular directives: mixing attribute data and ngModel - angularjs

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.

Related

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.

Being wrapped by a directive, how can I access its scope?

How can I access the directive's isolate scope in the directive's body? My DOM looks like this:
<div ng-app="app">
<directive>
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
The boolProperty is assigned inside the directive's link function:
angular.module("app", []).directive("directive", function() {
return {
scope: {},
link: function($scope) {
$scope.boolProperty = true;
}
};
});
The problem is, the child <p> inside the directive binds to the directive's parent scope, not the directive's isolated scope. How can I overcome this?
Click here for jsFiddle.
There are couple of problems in your code.
The default restrict option is A for attribute so anyways your directive will not be compiled because you are using it as an element. Use restrict: 'E' to make it work.
As per the documentation, the scope of the transcluded element is not a child scope of the directive but a sibling one. So boolProperty will always be undefined or empty. So you have to go up the scope level and find the proper sibling.
<div ng-app="app">
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty}}</p>
</directive>
</div>
and need to use transclusion in the directive as:
angular.module("app", []).directive("directive", function() {
return {
restrict: 'E',
scope: {},
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope) {
scope.boolProperty = true;
}
};
});
However, this approach is not advisable and break later If you add a new controller before the directive because transcluded scope becomes 2nd sibling unlike 1st as before.
<div ng-app="app">
<div ng-controller="OneCtrl"></div>
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty || $parent.$$childHead.$$nextSibling.boolProperty}}</p>
</directive>
</div>
Here is the Working Demo. The approach I mentioned is not ideal so use at your own risk. The #CodeHater' s answer is the one you should go with. I just wanted to explain why it did not work for you.
You forgot about two things:
By default AngularJS uses attrubute restriction, so in your case in directive definition you should specify restrict: "E"
You should use child scope, but not isolated. So set scope: true to inherit from parent view scope.
See updated fiddle http://jsfiddle.net/Y9g4q/1/.
Good luck.
From the docs:
As the name suggests, the isolate scope of the directive isolates everything except models that you've explicitly added to the scope: {} hash object. This is helpful when building reusable components because it prevents a component from changing your model state except for the models that you explicitly pass in.
It seems you would need to explicitly add boolProperty to scope.
<div ng-app="app" ng-controller="ctrl">
<directive bool="boolProperty">
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
JS
angular.module("app", []).controller("ctrl",function($scope){
$scope.boolProperty = false;
}).directive("directive", function() {
return {
restrict:"E",
scope: {boolProperty:'=bool'},
link: function($scope) {
$scope.boolProperty = "i'm a boolean property";
}
};
});
Here's updated fiddle.

Why I can't access the right scope?

html:
<!doctype html>
<html>
<head>
</head>
<body>
<div ng-app="project">
<div ng-controller="mainCtrl">
{{ property1 }}
<br />
{{ property2 }}
<div class="ts" d-child property1="{{ property1 }}cloud" property2="property2">
property1: {{ property1 }}
<br />
property2: {{ property2 }}
</div>
</div>
</div>
</body>
</html>
js:
angular.module('project', [])
.controller('mainCtrl', function($scope) {
$scope.property1 = 'ss';
$scope.property2 = 'dd';
});
angular.module('project')
.directive('dChild', function() {
return {
restrict: 'A',
scope: {
property1: '#',
property2: '='
},
link: function(scope, element, attrs) {
}
// template: '<input type="text" ng-model="property1" />'
}
})
I thought the property1: {{ property1 }} would be "property1: sscloud",but it turns out to be "ss",as if it still refers to the scope of the mainCtrl controller, shouldn't it be refer the scope of the d-child directive?
if I use template in the directive,it does refer to the right scope and shows 'sscloud',anyone can tell me why?
When angular compiles an element with isolated scope it has some rules:
If directives has no template property (or templateUrl), the inner content is attached to the parent scope. Actually before this commit, inner contents were getting the isolated scope. check your example to confirm it works on versions less than 1.2
If directives do have a template property then it would override the inner content (unless trancluded).
Only when you use a transclusion, then the inner content is attached to a sibling scope (non isolated).
The reason why angular works this way is to let reusable components be loosely coupled, and not have any side effects on your application.
Directives without isolate scope do not get the isolate scope from an isolate directive on the same element (see important commit).
Directive's template gets the isolated scope anyways.
If you want to alter this behavior you can pass the isolated scope to the tranclusion function like so:
angular.module('project')
.directive('dChild', function() {
return {
restrict: 'A',
transclude: true,
scope: {
property1: '#',
property2: '='
},
link: function(scope, element, attrs, ctrl, linker) {
linker(scope, function(clone, scope){
element.append(clone)
})
}
}
})
I highly recommend you to see these tutorials:
Angular.js - Transclusion basics
Angular.js - Components and containers
And to read more:
Access directive's isolate scope from within transcluded content
https://github.com/angular/angular.js/wiki/Understanding-Scopes
I'm not quite sure about this, I'm pretty sure it all has to do with when each {{}} is evaluated, and when the scope of the directive becomes isolated. My suggestion is to place the content in a template, as it seems to be working when doing so.
If you want to read more about the difference of of "#" and "=" in directive scopes, here's the best text I've found about it.
What is the difference between '#' and '=' in directive scope in AngularJS?
I think you have to use the transclude option.
In fact, as AngularJS docs says :
What does this transclude option do, exactly? transclude makes the contents of
a directive with this option have access to the scope outside of the directive
rather than inside.
Because of the Directives isolated scope that you created
More docs at:
http://docs.angularjs.org/guide/directive

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?

ng-repeat in combination with custom directive

I'm having an issue with using the ng-repeat directive in combination with my own custom directive.
HTML:
<div ng-app="myApp">
<x-template-field x-ng-repeat="field in ['title', 'body']" />
</div>
JS:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
compile: function(element, attrs, transcludeFn) {
element.replaceWith('<input type="text" />');
}
};
});
See jSFiddle
The problem here is that nothing is replaced. What I'm trying to accomplish is an output of 2x input fields, with the 'x-template-field' tags completely replaced in the DOM. My suspicion is that since ng-repeat is modifying the DOM at the same time, this won't work.
According to this Stack Overflow question, the accepted answer seems to indicate this has actually worked in earlier versions of AngularJS (?).
Wouldn't element.html('...') work?
While element.html('...') actually injects the generated HTML into the target element, I do not want the HTML as a child element of the template tag, but rather replace it completely in the DOM.
Why don't I wrap my template tag with another tag that has the ng-repeat directive?
Basically, for the same reason as above, I don't want my generated HTML as a child element to the repeating tag. While it would probably work decently in my application, I would still feel like I've adapted my markup to fit Angular and not the other way around.
Why am I not using the 'template' property?
I haven't found any way to alter the HTML retrieved from the 'template' / 'templateUrl' properties. The HTML I want to inject is not static, it's dynamically generated from external data.
Am I too picky with my markup?
Probably. :-)
Any help is appreciated.
Your directive needs to run before ng-repeat by using a higher priority, so when ng-repeat clones the element it is able to pick your modifications.
The section "Reasons behind the compile/link separation" from the Directives user guide have an explanation on how ng-repeat works.
The current ng-repeat priority is 1000, so anything higher than this should do it.
So your code would be:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
priority: 1001, <-- PRIORITY
compile: function(element, attrs, transcludeFn) {
element.replaceWith('<input type="text" />');
}
};
});
Put your ng-repeat in the template. You could modify attributes of element and accordingly in directive to determine if ng-repeat is needed, or what data to use inside the directive compiling
HTML(attribute):
<div ng-app="myApp" template-field></div>
JS:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'A',
template:'<input type="text" value="{{field}" ng-repeat="field in [\'title\',\'body\']" />'
};
});
DEMO: http://jsfiddle.net/GDfxd/3/
Also works as an element :
HTML(element):
<div ng-app="myApp" >
<template-field/>
</div>
JS
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
replace:true,
template:'<input type="text" value="{{field}}" ng-repeat="field in [\'title\',\'body\']" />'
};
});
DEMO: http://jsfiddle.net/GDfxd/3/

Resources