My dear friends,
We can bind a scope property of a directive to the value of DOM attribute.
This works:
module.directive 'MyDirective', ->
scope:
directiveVar: '='
...
<div class='MyDirective' directive-var='parentVar'></div>
In example above we bind directive's directiveVar property to parentVar property of the parent scope.
This is a bi-directional binding, so if directiveVar is changed parentVar is automatically updated, and vice versa.
My question is:
Is there a way I can bind a deep child property of my directive's scope instead? Like scope.lv1.directiveVar or scope.lv1.lv2.lv3.directiveVar instead of scope.directiveVar?
Docs I read
What I want to achieve
I have an object in directive scope named lv1. I want to bind its property directiveVar to parent property.
This does not work:
scope:
lv1:
directiveVar: '='
And this does not work:
scope:
"lv1.directiveVar": '=myVar'
Demo
This is what works: http://plnkr.co/edit/OClnZ2Cl3BXr60PC2qVP?p=preview
This is what I want to achieve: http://plnkr.co/edit/tQEHeKOzGjGyplCwUtU2?p=preview
I hope this code will help. You can pass in an object and watch its properties or you can nest things in parent/child directives. Either way adding the "=" will enable two way binding on the entire object.
Controller:
$scope.directiveVar = {
level1: {
greeting: 'Hello'
}
};
$scope.otherVar = {
levelA: {
foo: 'bar'
}
};
Markup:
<div data-my-parent-directive data-other-var="otherVar">
<div data-my-directive data-directive-var="directiveVar"></div>
</div>
Directive:
angular.module('MyDirective', [])
.directive('myParentDirective', ['$window',
function ($window) {
return{
restrict: 'AE',
scope:{
otherVar: '='
},
controller: function($scope, $element) {
var othis = this;
this.element = $element;
this.otherVar = $scope.otherVar;
}
};
}
])
.directive('myDirective', ['$window',
function ($window) {
return {
restrict: 'AE',
require: '?myParentDirective',
scope: {
directiveVar: '='
},
link: function(scope, elm, attr, myParentDirectiveCtrl) {
console.log(myParentDirectiveCtrl.otherVar);
console.log(myDirectiveParentCtrl.otherVar.levelA);
scope.$watch('directiveVar.level1', function (newValue, oldValue){
console.log(newValue, oldValue);
});
}
};
}
])
Edit:
You could simply do this:
<div data-my-parent-directive data-other-var="otherVar">
<div data-my-directive data-directive-var="directiveVar.level1"></div>
</div>
Which is useful when modeling data. For example:
var items = apiService.getItems();
var layers = [...],
$scope.map = {
points: items,
layers: layers,
highAltMap: 'Streets',
lowAltMap: 'Satellite'
};
That said if you are modelling data see this ng-conf video which I believe the speaker touched on some OO patterns.
Edit 2:
You could use a service like this plunker
I know date of the question, but...
Create json object in attribute
return {
scope: {
date: "=",
formChange: "=?",
ngDisabled: "=?",
deepInclude: "=?"
},
...
<user-date-without-current-date date="cv.dateBirthday" deep-include="{cnt:cv.dateBirthday}" form-change="$parent.form_change"></user-date-without-current-date>
Is it usefull?
Related
I have a tab-content component which gets a dynamic url from the controller
<tab-content url="{{tabContentUrl}}"></tab-content>
Directive of the component:
myApp.directive("tabContent", () => {
return {
restrict: "E",
scope: true,
link: (scope, element, attrs) => {
scope.getContentUrl = () => {
return attrs.url || "views/new-week-stats.html";
}
},
template: "<div ng-include='getContentUrl()'></div>"
}
});
At this point, it can only inject the initial values of the $scope in the controller. When something is changed in the scope it is not refreshed in the DOM. How can I enable two way binding in the directive?
ngInclude directive already does what you want for you, just use the directive scope 2 way bindings.
myApp.directive("tabContent", () => {
return {
restrict: "E",
scope: {
url: '='
},
template: '<div ng-include="url || 'views/new-week-stats.html'"></div>'
}
});
That way, any change in the url on the tabContent element will trigger a change in the ngInclude.
Quick Answer
You have two options.
Same scope as parent with two way binding (scope:false):
HTML:
<tab-content ng-model="tabContentUrl"></tab-content>
JS:
myApp.controller("MainCtrl", function($scope){
$scope.default = "views/new-week-stats.html";
$scope.tabContentUrl = "views/example.html";
});
myApp.directive("tabContent", () => {
return {
restrict: "E",
scope: false,
template: "<div ng-include='tabContentUrl || default'></div>"
}
});
Isolated scope with two way binding (scope:{url: '='}):
HTML:
<div ng-controller="MainCtrl">
<tab-content url="tabContentUrl" default="views/new-week-stats.html">
</tab-content>
</div>
JS:
myApp.controller("MainCtrl", function($scope){
$scope.tabContentUrl = "views/example.html"
});
myApp.directive("tabContent", () => {
return {
restrict: "E",
scope: {
url: '=',
default: '#'
},
template: "<div ng-include='url || default'></div>"
}
});
Explanation:
All directives have a scope associated with them. They use this scope for accessing data/methods inside the template and link function. By default, unless explicitly set, directives don’t create their own scope. Therefore, directives use their parent scope (usually a controller) as their own.
In your code on line 4 you specified scope: true,
When a directive scope is set to “true”, AngularJS will create a new scope object and assign to the directive. This newly created scope object is prototypically inherited from its parent scope (the controller scope where it’s been used).
In total there are three options:
Same scope as parent with two way binding.
scope: false,
Isolated scope but a one way binding.
scope: true,
Isolated scope with options.
scope: {
"#" // Creates a copy as text
"=" // Direct model with two-way binding
"&" // For passing functions
}
add a watcher in your directive:
HTML
<tab-content url="tabContentUrl"></tab-content>
JS
myApp.directive( "tabContent", () => {
return {
restrict: "E",
scope : true,
link : ( scope, element, attrs ) => {
scope.$watch( () => scope.$eval( attrs.url ), newValue => {
scope.url = newValue;
} );
},
template: "<div ng-include='url'></div>"
};
} );
You can also use $parse instead of $eval, but you don't got an isolated scope, so it should be ok, as long as you don't overwrite the parameters in your directive
You can use an observer to dynamically change the scope value inside your directive.
link: function(scope, element, attrs){
attrs.$observe('tabContentUrl', function(val) {
scope.tabContentUrl = val;
})
}
I want to pass object (reference - two way binding) through ATTRIBUTE not by isolated scope. How can I do this? Because code bellow passing string instead of object:
HTML
<tr ng-form="rowForm" myDirective="{{row.data}}">
Directive
angular.module("app").directive("myDirective", function () {
return {
require: ["^form"],
restrict: "A",
link: function (scope, element, attrs, ctrls) {
scope.$watch(function () {
return attrs.myDirective;
}, function (newValue, oldValue) {
// .....
Directives can do two-way data binding without parsing or compile anything manually, sorry for not delivering the plunker but it's rebelius and won't save for me
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.myObj = {name: 'Tibro', age: 255}
})
.directive('myDirective', function(){
return {
scope: {
'myAttribute': '='
},
template: '{{myAttribute}}',
link: function(scope){
scope.myAttribute.age= 31
}
}
})
HTML
<body ng-controller="MainCtrl">
controller: {{myObj}} <br/>
directive: <my-directive my-attribute="myObj"></my-directive>
</body>
OUTPUT
controller: {"name":"Tibro","age":31}
directive: {"name":"Tibro","age":31}
you can see from the output that passed object has been binded two-way and change made in directive is reflected on controller level
The result of {{ }} interpolation is a string. An object can't be passed like that.
Bindings are idiomatic here and thus preferable. The whole thing becomes messy when the directive is forced to use parent scope. However, it can be done by parsing scope properties manually with $parse:
$scope.$watch(function () {
var myDirectiveGetter = $parse($attrs.myDirective);
return myDirectiveGetter($scope);
}, ...);
This is a job for binding (< or =, depending on the case). If isolated scope isn't desirable, this can be done with inherited scope and bindToController:
scope: true,
bindToController: {
myDirective: '<'
},
controllerAs: `vm`,
controller: function ($scope) {
$scope.$watch('vm.myDirective', ...);
}
Notice that directive attribute is my-directive, not myDirective.
Couple of things:
myDirective in the tr should be my-directive, as per Angular's
conventions.
{{row.data}} prints the variable, you need to pass it without the
{{}} for it to go through as an object.
I'm wondering if there is a way to require the controller of a directive that exists/is nested somewhere as a common parent's child directive in AngularJS.
Directive Structure
Suppose I have the following structure for my directives:
<parent-directive>
<ul>
<li some-nested-directive ng-repeat="dir in directives"></li>
</ul>
<settings-menu></settings-menu>
</parent-directive>
Directive Definition
/*
* some-nested-directive directive definition
*/
.directive('someNestedDirective', function(){
// ...
return {
restrict: 'A',
controller: someNestedDirectiveController
};
});
/*
* settings-menu directive definition
*/
.directive('settingsMenu', function(){
return {
restrict: 'AE',
require: [], // how to require the nested-directive controller here?
link: function(scope, iElement, attrs, ctrls){
// ...
}
};
})
I've already checked out this SO question which states how to require controllers of directives that exist along the same line in a hierarchy.
But my question is regarding a way to do the same in a hierarchy of directives that NOT necessarily exist along the same line. And if this is not possible, what is a proper workaround for it. Any help would be appreciated.
EDIT
Also, can any of the prefixes for require (or a combination of them) be used to achieve the same?
One approach is to use parent directive as a way to pass references between controllers:
var mod = angular.module('test', []);
mod.directive('parent', function() {
return {
restrict: 'E',
transclude: true,
template: '<div>Parent <div ng-transclude=""></div></div>',
controller: function ParentCtrl() {}
}
});
mod.directive('dirA', function() {
return {
restrict: 'E',
template: '<div>Dir A <input type="text" ng-model="name"></div>',
require: ['dirA', '^^parent'],
link: function(scope, element, attrs, ctrls) {
//here we store this directive controller into parent directive controller instance
ctrls[1].dirA = ctrls[0];
},
controller: function DirACtrl($scope) {
$scope.name = 'Dir A Name';
this.name = function() {
return $scope.name;
};
}
}
});
mod.directive('dirB', function() {
return {
restrict: 'E',
template: '<div>Dir A <button ng-click="click()">Click</button></div>',
require: ['dirB', '^^parent'],
link: function(scope, element, attrs, ctrls) {
//let's assign parent controller instance to this directive controller instance
ctrls[0].parent = ctrls[1];
},
controller: function DirBCtrl($scope) {
var ctrl = this;
$scope.click = function() {
//access dirA controller through parent
alert(ctrl.parent.dirA.name());
};
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app='test'>
<parent>
<dir-a></dir-a>
<dir-b></dir-b>
</parent>
</div>
When using above approach it also makes sense to encapsulate how the dirA controller is stored inside parent controller i.e. by using a getter property or by exposing only the required properties of dirA controller.
I aggree with miensol's reply and I recommend that approach but in some cases you may need something like that;
<parent-directive>
<ul>
<some-nested-directive id="snd1" ng-repeat="dir in directives"></some-nested-directive>
</ul>
<settings-menu some-nested-directive-id="snd1"></settings-menu>
You can access the scope of some-nested-directive using its id from the settings-menu;
$("#" + scope.someNestedDirectiveId).scope()
Once I used this approach to cascade the values of a dropdown according to the choise of another independent dropdown.
Im having a hard time accessing the attributes passed in to my directive from the template of that directive. I want to be able to access 'companyId' from album.tmpl.html but no matter what i try i can't get it. The strangest part is i can see it has made its way in to the controller, but somehow it's not getting from the controller to the template. I know the template is correctly calling the controller as it can succesfully print out the value of 'testVar' which is initialised inside the controller. Any advice would be appreciated.
directive + directive controller
(function () {
'use strict';
angular.module('erCommon')
.directive('erAlbum', albumDirective)
.controller('AlbumController', AlbumController);
function AlbumController() {
var vm = this;
vm.testVar = "test var initiated";
}
function albumDirective($log) {
function albumLink(scope, element, attrs, AlbumController) {
//watch vars in here
}
return {
restrict: 'E',
scope: {
companyId: '=companyId'
},
bindToController: true,
templateUrl: 'components/temp/album.tmpl.html',
controller: 'AlbumController',
controllerAs: 'albumCtrl',
link: albumLink
};
}
})();
template ( album.tmpl.html
<div ng-controller="AlbumController as albumCtrl">
testVar: {{albumCtrl.testVar}}<BR>
companyId:{{albumCtrl.companyId}}<BR>
</div>
usage
<er-album company-id="2"></er-album>
output
test var: test var initiated
companyId:
You need to remove ng-controller from your template:
<div>
testVar: {{albumCtrl.testVar}}<BR>
companyId:{{albumCtrl.companyId}}<BR>
</div>
To achieve the result you wanted i had to modify the structure of your code slightly. Hope this helps you to understand the issue. Look for materials about isolated scopes which Angular uses with directives.
HTML:
<div ng-app="erCommon" ng-controller="AlbumController as albumCtrl">
<er-album company-id="2" test = "albumCtrl.testVar"></er-album>
</div>
Controller:
angular.module('erCommon', [])
.directive('erAlbum', albumDirective)
.controller('AlbumController', AlbumController);
function AlbumController() {
var vm = this;
vm.testVar = "test var initiated";
}
function albumDirective() {
return {
restrict: 'E',
scope: {
test: '=test',
companyId: '#companyId'
},
template: '<div> testVar: {{test}}<BR> companyId:{{companyId}}<BR> </div>', // it will work fine with templateUrl as well, just didn't want to cr8 another file...
link: function(scope, element, attrs){
//do whatever else you might need;
}
};
}
I have read some posts here on so regarding this subject but all the answers and all of the fiddles I saw not one working properly. I got my plunkr to bind one-way from the dynamically generated field bound through ng-model inside an directive isolated scope to the form in the parent. However is does not bind from the form back to the directive. My initial values are disregarded, so are the changes I make inside the parent scope and I would really like a solution. Here's a code fragment.
<div ng-repeat="field in fields">
<app-field field="field" ng-model="selected[field.name]" form="form"></app-field>
</div>
...
.directive("appField", function($http, $compile) {
return {
restrict: "AE",
scope: {
field: "=",
ngModel: "=",
form: "="
},
link: function($scope, element, attrs) {
$http.get("field.html").then(function (result) {
element.html(result.data);
$compile(element.contents())($scope);
});
}
}
})
Here's my fiddle: http://plnkr.co/edit/ZzC4jS9M9Ev5i6gxUVxB?p=preview
Any help would be appreciated.
The property is scope, not $scope. Then, remove the scope:false and you are done!
Also, you can use the templateUrl property instead of link (in this case):
.directive("appField", function($http, $compile) {
return {
restrict: "AE",
scope: {
field: "=",
ngModel: "=",
form: "="
},
templateUrl: 'field.html'
}
})