The main point of this post is that I want to avoid using $scope.$watch as it is taught to cause a performance decrease.
So imagine having one shared view partial/template, call it "mypage" with two different directives, call them "directive1" and "directive2", in it that share a data model, lets call it "awesomeData"
Maybe it looks something like this:
<div class="mypage-root">
<directive1 shared-data="awesomeData"></directive1>
<directive2 shared-data="awesomeData"></directive2>
</div>
Now obviously when "awesomeData" changes in either directive or the root view, the data changes in the other parts too (assuming it is two-way bound).
But what if i want something else to happen in directive2 when directive1 has updated the data model, like calling a function in directive2?
I could use a watcher but as mentioned, that is a performance decrease.
What other approaches are there and what is the "true" angular way to do this?
I know you're asking about how to do this without a watcher, but I think in this instance a watcher is the best answer as it gives separation of concerns. directive1 shouldn't have to know about directive2, but only about MySharedService and anything it might broadcast:
$rootScope.$broadcast('myVar.updated');
...
$scope.$on('myVar.updated', function (event, args) {
});
Edit:
Also, depending on the complexity of the model the directives will be using, you could pass this model from their parent controller (The down side to this is that you have to instantiate a model from a parent):
HTML:
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angularjs#1.4.9" data-semver="1.4.9" src="https://code.angularjs.org/1.4.9/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller="MyCtrl">
<directive1 my-model="model"></directive1>
<directive2 my-model="model"></directive2>
</body>
</html>
JS:
var app = angular.module('app', []);
app.controller('MyCtrl', function($scope) {
$scope.model = { info: 'Starting...' };
});
app.directive('directive1', function() {
var controllerFn = function($scope) {
$scope.changeInfo = function() {
$scope.vm.myModel.info = 'Directive 1';
};
};
return {
restrict: 'E',
controller: controllerFn,
template: '<div><span>hello</span> <span ng-bind="vm.myModel.info"></span> <button ng-click="changeInfo()">Click</button></div>',
controllerAs: 'vm',
bindToController: true,
scope: {
myModel: '='
}
}
});
app.directive('directive2', function() {
var controllerFn = function($scope) {
$scope.changeInfo = function() {
$scope.vm.myModel.info = 'Directive 2';
};
};
return {
restrict: 'E',
controller: controllerFn,
template: '<div<span>bye</span> <span ng-bind="vm.myModel.info"></span> <button ng-click="changeInfo()">Click</button></div>',
controllerAs: 'vm',
bindToController: true,
scope: {
myModel: '='
}
}
});
plunker: https://plnkr.co/edit/EwdHFl7eZl2rd43UVG4J?p=preview
Edit 2:
As with method 2, you could have a shared service, that provides this model:
plunker: https://plnkr.co/edit/EwdHFl7eZl2rd43UVG4J?p=preview
Related
When looking for information regarding Angular directives and passing behavior to directives, I get ended up being pointed in the direction of method binding on an isolate scope, i.e.
scope: {
something: '&'
}
The documentation for this functionality is a bit confusing, and I don't think it'll end up doing what I want.
I ended up coming up with this snippet (simplified for brevity), that works by passing a scope function in HomeCtrl, and the directive does it's work and calls the function. (Just incase it matters, the real code passes back a promise from the directive).
angular.module('app', []);
angular.module('app')
.directive('passingFunction',
function() {
var changeFn,
bump = function() {
console.log('bump() called');
internalValue++;
(changeFn || Function.prototype)(internalValue);
},
internalValue = 42;
return {
template: '<button ng-click="bump()">Click me!</button>',
scope: {
onChange: '<'
},
link: function(scope, element, attrs) {
if (angular.isFunction(scope.onChange)) {
changeFn = scope.onChange;
}
scope.bump = bump;
}
};
})
.controller('HomeCtrl',
function($scope) {
$scope.receive = function(value) {
console.log('receive() called');
$scope.receivedData = value;
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.4/angular.min.js"></script>
<div ng-app="app" ng-controller="HomeCtrl">
<passing-function on-change="receive"></passing-function>
<p>Data from directive: {{receivedData}}</p>
</div>
Is this a proper "Angular" way of achieving this? This seems to work.
What you need is to pass the function to the directive. I'll make a very small example.
On controller:
$scope.thisFn = thisFn(data) { console.log(data); };
In html:
<my-directive passed-fn="thisFn()"></my-directive>
On directive:
.directive('myDirective', [
() => {
return {
restrict: 'E',
scope: {
passFn: '&'
},
template: '<div id="myDiv" ng-click="passFn(data)"></div>',
link: (scope) => {
scope.data = "test";
}
}
}]);
I have a parent directive in which its controller makes a call through a service to get some data.
sharedService.getData(options).then(function(data){
$scope.data = data;
});
Now i need this data in my child controller.
What i have already tried are the ff:
1) Through $timeout i get the data after sometime but it doesn't seem a good solution impacting performance
2) watchCollection() - i watched if newValue !== oldValue
problem being the data is huge so it takes a toll of performance
Now the issue i'm getting is the child directive gets executed after parent BUT before the data comes back from the service and i'm not able to get that data in my child directive via $scope.data.
Is there any solution to get data from parent directive to child directive when i have to wait for data to come in parent?
You can include your parent directive controller in your child directive by using require.
angular.module('myApp', [])
.directive('dirParent', function() {
return {
restrict: 'E',
scope: {},
controller: ['$scope', function($scope) {
}],
};
})
.directive('dirChild', function() {
return {
require: '^dirParent', // include directive controller
restrict: 'E',
scope: {
},
link: function(scope, element, attrs, paretCtrl) {
var data = paretCtrl.getMyData();
}
};
})
It's always a best to use service for communication and and business logic. Here is an example. Please check. This might solve your problem.
// Code goes here
angular.module('app', [])
.factory('messageService', function() {
return {
message: null
}
})
.directive('parentDir', function() {
return {
scope: {}, //isolate
template: '<input type="text" ng-model="PDirInput"/><button ng-click="send()">Send</button>',
controller: function($scope, messageService) {
$scope.send = function() {
messageService.message = $scope.PDirInput;
}
}
}
})
.directive('childDir', function() {
return {
scope: {}, //isolate
template: '<code>{{CDirInput.message}}</code>',
controller: function($scope, messageService) {
$scope.CDirInput = messageService;
}
}
})
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angular.js#*" data-semver="2.0.0-alpha.31" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<HR/>Parent Directive
<parent-dir></parent-dir>
<br/>
<HR/>Child Directive
<child-dir></child-dir>
<HR/>
</body>
</html>
So, I'm not sure what it is I'm asking, but I want to achieve this:
Index.html:
<div ng-view>
</div>
<script>
angular.module('myApp', ['ngRoute'])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', { controller: "HomeController", templateUrl: '/Partials/Home/Dashboard.html' });
$routeProvider.otherwise({ redirectTo: '/' });
}]);
</script>
Home/Dashboard.html:
<h2 class="page-header">{{welcome}}</h2>
<!-- Insert my reusable component here -->
My reusable component would reside in MyComponent/Component.html and have the controller myApp.component.controller associated with it.
So effectively I want to drop in the resuable component into the page and bind it to my controller. So I got as far as having this:
.directive('MyComponent', function() {
return {
restrict: 'E',
scope: {
},
templateUrl: '/MyComponent/Component.html'
};
});
So how do I now bind my controller to it? Do I do this:
.directive('MyComponent', function() {
return {
restrict: 'E',
resolve: function () {
return /* resolve myApp.component.controller */;
},
templateUrl: '/MyComponent/Component.html'
};
});
When a directive requires a controller, it receives that controller as the fourth argument of its
link function.
.directive('MyComponent', function() {
return {
restrict: 'E',
controller: 'MyController', // attach it.
require: ['MyController','^ngModel'], // required in link function
templateUrl: '/MyComponent/Component.html',
link: function(scope, element, attrs, controllers) {
var MyController = controllers[0];
var ngModelCtlr = controllers[1];
///...
}
};
});
The ^ prefix means that this directive searches for the controller on its parents (without the ^ prefix, the directive would look for the controller on just its own element).
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.
You can assign controllers to directives:
.directive('MyComponent', function() {
return {
restrict: 'E',
controller : 'HomeController',
templateUrl: '/MyComponent/Component.html'
};
});
So I just want to clarify a few things up here.
/MyComponent/Component.html:
<h2>{{welcome}}</h2>
/mycomponent.directive.js:
.directive('MyComponent', function() {
return {
restrict: 'E',
controller : 'ComponentController',
templateUrl: '/MyComponent/Component.html'
};
});
the above bound like this in index.html:
<h2>{{welcome}}</h2> <!-- this is from ng-controller = HomeController -->
<my-component></my-component> <!-- this is scoping from ng-controller = ComponentController -->
This generates the result
<h2>Hello from MyComponent</h2>
<h2>Hello from MyComponent</h2>
It appears that the ComponentController has taken over the entire scope. I then changed the directive to this:
.directive('MyComponent', function() {
return {
restrict: 'E',
// controller : 'ComponentController',
templateUrl: '/MyComponent/Component.html'
};
});
And changed the index.html to this:
<h2>{{welcome}}</h2> <!-- this is from ng-controller = HomeController -->
<div ng-controller="ComponentController">
<my-component></my-component> <!-- this is scoping from ng-controller = ComponentController -->
</div>
This gave the correct output:
<h2>Welcome from HomeController</h2>
<h2>Welcome from ComponentController</h2>
Then I changed the directive again to this:
.directive('MyComponent', function() {
return {
restrict: 'A', // <----- note this small change, restrict to attributes
// controller : 'ComponentController',
templateUrl: '/MyComponent/Component.html'
};
});
I changed index.html to this:
<h2>{{welcome}}</h2>
<div ng-controller="ComponentController" my-component></div>
And this also produced the desired output, just in less lines of code.
So I just hope this clarifies directives to people a bit better. I put this on for completeness and the steps that I took to achieve this. Obviously I had some help from the other answers which pointed me in the right direction.
JSBin Example:
http://jsbin.com/yuyetakonowu/1/edit?html,js,output
Summary:
I have two directive's (myParentDirective and myChildDirective). myParentDirective transcludes myChildDirective content. I am attempting to two-way-bind a model object within myChildDirective. It works successfully when I "update" the object by simply changing or adding properties to an existing object instance. However, when I "assign" a new object (using equals operator from the controller's timeout function) myChildDirective will not be updated.
HTML:
<html ng-app='ValidationApp'>
<head>
<title>Assigning a model object after isolated scope is set doesn't work</title>
</head>
<body ng-controller='MyController'>
<h2>MyController.assignedObject: {{assignedObject}}</h2>
<h2>MyController.updatedObject: {{updatedObject}}</h2>
<my-parent-directive assigned-object='assignedObject' updated-object='updatedObject'>
<my-child-directive></my-child-directive>
</my-parent-directive>
<script src='//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.17/angular.js'></script>
<script src='app.js'></script>
</body>
</html>
JavaScript:
var app = angular.module('ValidationApp', [])
app.controller('MyController', [
'$scope',
'$http',
function($scope, $http) {
// Model objects loaded on page-load
$scope.assignedObject = {value: 'pre-update'}
$scope.updatedObject = {value: 'pre-update'}
// Mock ajax request
setTimeout(function() {
// This is what I'm ultimately trying to accomplish. However, myChildDirective is not properly
// showing the updated value 'post-update'.
$scope.assignedObject = {value: "post-update"}
// I noticed that this line will properly update myChildDirective, but it's not an ideal solution.
// I'm including it in the example just to show the inconsistent results in myChildDirective.
$scope.updatedObject.value = "post-update"
$scope.$apply()
}, 1000)
}
])
app.directive('myParentDirective', function() {
return {
restrict: 'E',
transclude: true,
scope: {
assignedObject: '=',
updatedObject: '='
},
template: '\
<h2>myParentDirective.assignedObject: {{assignedObject}}</h2>\
<h2>myParentDirective.updatedObject: {{updatedObject}}</h2>\
<div ng-transclude></div>\
',
controller: function($scope, $element) {
this.assignedObject = $scope.assignedObject
this.updatedObject = $scope.updatedObject
}
}
})
app.directive('myChildDirective', function() {
return {
restrict: 'E',
require: '^myParentDirective',
scope: false,
template: '\
<h2>myChildDirective.myParentDirective.assignedObject: {{myParentDirective.assignedObject}}</h2>\
<h2>myChildDirective.myParentDirective.updatedObject: {{myParentDirective.updatedObject}}</h2>\
',
link: function($scope, $element, $attrs, myParentDirective) {
$scope.myParentDirective = myParentDirective
}
}
})
I found a few solutions to my problem...
The problem is that I'm assigning $scope.assignedObject to this.assignedObject within myParentDirective. When I do this, there's no way for myChildDirective to know when the property has changed. Ordinarily, $scope.$apply() function would be called to notify all observers that a scope property has changed, but since I'm re-assigning this object reference to this.assignedObject myChildDirective never receives that event.
The simplest solution can be found here: http://jsbin.com/yuyetakonowu/11/edit. Basically, this is simply inheriting the parent scope so that I can rely on angular's scope to emit the appropriate event and update myChildDirective accordingly.
However, this wasn't good enough for me as I also needed myChildDirective to have an isolated scope with its own properties. This means I can't simply "inherit" the parent scope. I've resolved this issue with the following: http://jsbin.com/yuyetakonowu/9/edit.
The end result:
HTML:
<html ng-app='ValidationApp'>
<head>
<title>Assigning a model object after isolated scope is set doesn't work</title>
</head>
<body ng-controller='MyController'>
<h1>MyController</h1>
<h2>assignedObject: {{assignedObject}}</h2>
<h2>updatedObject: {{updatedObject}}</h2>
<my-parent-directive assigned-object='assignedObject' updated-object='updatedObject'>
<my-child-directive child-property='child-property-value'></my-child-directive>
</my-parent-directive>
<script src='//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.17/angular.js'></script>
<script src='app.js'></script>
</body>
</html>
JavaScript:
var app = angular.module('ValidationApp', [])
app.controller('MyController', [
'$scope',
'$http',
function($scope, $http) {
// Model objects loaded on page-load
$scope.assignedObject = {value: 'pre-update'}
$scope.updatedObject = {value: 'pre-update'}
// Mock ajax request
setTimeout(function() {
// This is what I'm ultimately trying to accomplish. However, myChildDirective is not properly
// showing the updated value 'post-update'.
$scope.assignedObject = {value: "post-update"}
// I noticed that this line will properly update myChildDirective, but it's not an ideal solution.
// I'm including it in the example just to show the inconsistent results in myChildDirective.
$scope.updatedObject.value = "post-update"
$scope.$apply()
}, 1000)
}
])
app.directive('myParentDirective', function() {
return {
restrict: 'E',
transclude: true,
scope: {
assignedObject: '=',
updatedObject: '='
},
template: '\
<h1>myParentDirective</h1>\
<h2>assignedObject: {{assignedObject}}</h2>\
<h2>updatedObject: {{updatedObject}}</h2>\
<div ng-transclude></div>\
',
controller: function($scope, $element) {
// Generally, exposing isolate scope is considered bad practice. However, this directive is intended
// to be used with child directives which explicitly depend on this directive. In addition, child
// directives will likely need their own isolated scope with two-way binding of properties on this scope.
this._scope = $scope
}
}
})
app.directive('myChildDirective', function() {
return {
restrict: 'E',
require: '^myParentDirective',
scope: {
childProperty: '#'
},
template: '\
<h1>myChildDirective</h1>\
<h2>childProperty: {{childProperty}}</h2>\
<h2>assignedObject: {{assignedObject}}</h2>\
<h2>updatedObject: {{updatedObject}}</h2>\
',
link: function($scope, $element, $attrs, myParentDirective) {
myParentDirective._scope.$watch('assignedObject', function(newValue, oldValue) {
$scope.assignedObject = newValue
})
myParentDirective._scope.$watch('updatedObject', function(newValue, oldValue) {
$scope.updatedObject = newValue
})
}
}
})
Can anyone tell me how to include a controller from one directive in another angularJS directive.
for example I have the following code
var app = angular.module('shop', []).
config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', {
templateUrl: '/js/partials/home.html'
})
.when('/products', {
controller: 'ProductsController',
templateUrl: '/js/partials/products.html'
})
.when('/products/:productId', {
controller: 'ProductController',
templateUrl: '/js/partials/product.html'
});
}]);
app.directive('mainCtrl', function () {
return {
controller: function ($scope) {}
};
});
app.directive('addProduct', function () {
return {
restrict: 'C',
require: '^mainCtrl',
link: function (scope, lElement, attrs, mainCtrl) {
//console.log(cartController);
}
};
});
By all account I should be able to access the controller in the addProduct directive but I am not. Is there a better way of doing this?
I got lucky and answered this in a comment to the question, but I'm posting a full answer for the sake of completeness and so we can mark this question as "Answered".
It depends on what you want to accomplish by sharing a controller; you can either share the same controller (though have different instances), or you can share the same controller instance.
Share a Controller
Two directives can use the same controller by passing the same method to two directives, like so:
app.controller( 'MyCtrl', function ( $scope ) {
// do stuff...
});
app.directive( 'directiveOne', function () {
return {
controller: 'MyCtrl'
};
});
app.directive( 'directiveTwo', function () {
return {
controller: 'MyCtrl'
};
});
Each directive will get its own instance of the controller, but this allows you to share the logic between as many components as you want.
Require a Controller
If you want to share the same instance of a controller, then you use require.
require ensures the presence of another directive and then includes its controller as a parameter to the link function. So if you have two directives on one element, your directive can require the presence of the other directive and gain access to its controller methods. A common use case for this is to require ngModel.
^require, with the addition of the caret, checks elements above directive in addition to the current element to try to find the other directive. This allows you to create complex components where "sub-components" can communicate with the parent component through its controller to great effect. Examples could include tabs, where each pane can communicate with the overall tabs to handle switching; an accordion set could ensure only one is open at a time; etc.
In either event, you have to use the two directives together for this to work. require is a way of communicating between components.
Check out the Guide page of directives for more info: http://docs.angularjs.org/guide/directive
There is a good stackoverflow answer here by Mark Rajcok:
AngularJS directive controllers requiring parent directive controllers?
with a link to this very clear jsFiddle: http://jsfiddle.net/mrajcok/StXFK/
<div ng-controller="MyCtrl">
<div screen>
<div component>
<div widget>
<button ng-click="widgetIt()">Woo Hoo</button>
</div>
</div>
</div>
</div>
JavaScript
var myApp = angular.module('myApp',[])
.directive('screen', function() {
return {
scope: true,
controller: function() {
this.doSomethingScreeny = function() {
alert("screeny!");
}
}
}
})
.directive('component', function() {
return {
scope: true,
require: '^screen',
controller: function($scope) {
this.componentFunction = function() {
$scope.screenCtrl.doSomethingScreeny();
}
},
link: function(scope, element, attrs, screenCtrl) {
scope.screenCtrl = screenCtrl
}
}
})
.directive('widget', function() {
return {
scope: true,
require: "^component",
link: function(scope, element, attrs, componentCtrl) {
scope.widgetIt = function() {
componentCtrl.componentFunction();
};
}
}
})
//myApp.directive('myDirective', function() {});
//myApp.factory('myService', function() {});
function MyCtrl($scope) {
$scope.name = 'Superhero';
}