I am creating a directive where two other child directives use it is functionalities but I need a help to know how to make the scope shared between the parent and the child directive
Parent directive
MetronicApp.directive('atmSearchComponent', function () {
function AtmSearchComponentController(scope) {
// initialize scope
}
AtmSearchComponentController.prototype.search = function () {
scope.mainGridOptions.dataSource.page(1);
reset();
}
AtmSearchComponentController.prototype.clearSearch = function () {
scope.data.basicSearch = {};
scope.data.advancedSearch = {};
}
AtmSearchComponentController.prototype.keyPressSearch = function (keyEvent) {
if (keyEvent.which === 13) {
keyEvent.preventDefault()
scope.search();
}
};
return {
restrict: 'E',
controller: ['$scope', AtmSearchComponentController],
scope: {}
};
});
Child directive
MetronicApp.directive('atmsQuerySelector', function ($log, $parse, $timeout, $http, $uibModal) {
return {
restrict: "E",
replace: false,
scope: {},
require: ['^atmSearchComponent', 'ngModel'],
templateUrl: "/apps/framework/templates/atmsSelector.view",
link: function (scope, element, attrs, controllersArray) {
scope.search = controllersArray[0].search;
clearSearch = controllersArray[0].clearSearch;
scope.keyPressSearch = controllersArray[0].keyPressSearch;
},
}
});
but when I call the search method I get the following error
ReferenceError: scope is not defined
at n.AtmSearchComponentController.search (directives.js:8)
Related
app.directive("itemsContainer", function() {
return {
restrict: "E",
controllerAs: "itc",
bindToController: true,
controller: function() {
this.showItems = false;
this.items = null;
this.loadItems = (data) => {
this.items = data;
this.showItems = true;
}
this.hideSummary = () => {
this.showItems = false;
}
},
templateURL:'itemsContainer.html'
};
});
app.directive("itemsSummary", function() {
return {
restrict: "E",
require: "^itc",
link: function(scope, element, attrs, ctrl) {
scope.showSummary = ctrl.showItems;
scope.items = ctrl.items;
},
templateURL:'itemsSummary.html'
};
});
app.directive("itemsList", function() {
return {
restrict: "E",
require: "^itc",
scope: {
items = "="
},
link: function(scope, element, attrs, ctrl) {
if(items !== null)
{
ctrl.loadItems(items);
}
scope.hideSummary = () => {
ctrl.hideSummary();
}
},
templateURL:'itemsList.html'
};
});
<itemsContainer>
<itemsSummary>{{itemsSummary}}</itemsSummary>
<itemsList>{{items}}</itemsList>
</itemsContainer>
Here, when itemsList directive set the Hide summary using itemsContainer controller, which is not updated in itemsSummary?
how to make all the three directive in sync?
Best way to communicate between sibling directive?
Currently am doing with Event emit which I don't want to do.
I need a best practice solution.
My requirement:
<parent>
<child1></child1>
<child2></child2>
</parent>
How to communicate any update in child2 to child1?
You need to do manual transclution, that's what I did in a similar situation. Anglers default transclution won't work, since it creates a new scope.
<itemsContainer>
<itemsSummarydata="$ctrl.data"></itemsSummary>
</itemsContainer>
/** * Parent component */
angular.module("app").component("itemsContainer", function() {
constructor() {
return {
restrict: 'E',
replace: true,
template: "<div transclude-extended></div>",
transclude: true,
bindToController: true,
controller: function () {
var ctrl = this;
ctrl.data = 'This is sample data.';
}
};
}
});
/** * Transclude Extended Directive */
angular.module("app").directive("transcludeExtended", function(){
constructor() {
return {
link: function ($scope, $element, $attrs, controller, $transclude) {
var innerScope = $scope.$new();
$transclude(innerScope, function (clone) {
$element.empty();
$element.append(clone);
$element.on('$destroy', function () {
innerScope.$destroy();
});
});
}
}
};
});
transcludeExtended is the manual way of doing the translation, instead of ng-transclude
Reference: https://github.com/angular/angular.js/issues/1809
I am having a hard time trying to figure out how I mock out a required controller for a directive I have written that's the child of another.
First let me share the directives I have:
PARENT
angular
.module('app.components')
.directive('myTable', myTable);
function myTable() {
var myTable = {
restrict: 'E',
transclude: {
actions: 'actionsContainer',
table: 'tableContainer'
},
scope: {
selected: '='
},
templateUrl: 'app/components/table/myTable.html',
controller: controller,
controllerAs: 'vm',
bindToController: true
};
return myTable;
function controller($attrs, $scope, $element) {
var vm = this;
vm.enableMultiSelect = $attrs.multiple === '';
}
}
CHILD
angular
.module('app.components')
.directive('myTableRow', myTableRow);
myTableRow.$inject = ['$compile'];
function myTableRow($compile) {
var myTableRow = {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
return myTableRow;
function link(scope, element, attrs, ctrls) {
var self = ctrls.shift(),
tableCtrl = ctrls.shift();
if(tableCtrl.enableMultiSelect){
element.prepend(createCheckbox());
}
self.isSelected = function () {
if(!tableCtrl.enableMultiSelect) {
return false;
}
return tableCtrl.selected.indexOf(self.model) !== -1;
};
self.select = function () {
tableCtrl.selected.push(self.model);
};
self.deselect = function () {
tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
};
self.toggle = function (event) {
if(event && event.stopPropagation) {
event.stopPropagation();
}
return self.isSelected() ? self.deselect() : self.select();
};
function createCheckbox() {
var checkbox = angular.element('<md-checkbox>').attr({
'aria-label': 'Select Row',
'ng-click': 'vm.toggle($event)',
'ng-checked': 'vm.isSelected()'
});
return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
}
}
function controller() {
}
}
So as you can probably see, its a table row directive that prepends checkbox cells and when toggled are used for populating an array of selected items bound to the scope of the parent table directive.
When it comes to unit testing the table row directive I have come across solutions where can mock required controllers using the data property on the element.
I have attempted this and am now trying to test the toggle function in my table row directive to check it adds an item to the parent table directive's scope selected property:
describe('myTableRow Directive', function() {
var $compile,
scope,
compiledElement,
tableCtrl = {
enableMultiSelect: true,
selected: []
},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_.$new();
$compile = _$compile_;
});
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
scope.data = {foo: 'bar'};
compiledElement = $compile(element)(scope);
scope.$digest();
controller = compiledElement.controller('myTableRow');
});
describe('select', function(){
it('should work', function(){
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
But I'm getting an error:
undefined is not an object (evaluating 'controller.toggle')
If I console log out the value of controller in my test it shows as undefined.
I am no doubt doing something wrong here in my approach, can someone please enlighten me?
Thanks
UPDATE
I have come across these posts already:
Unit testing a directive that defines a controller in AngularJS
How to access controllerAs namespace in unit test with compiled element?
I have tried the following, given I'm using controllerAs syntax:
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
$compile(element)($scope);
$scope.$digest();
console.log(element.controller('vm'));
But the controller is still coming up as undefined in the console log.
UPDATE 2
I have come across this post - isolateScope() returning undefined when testing angular directive
Thought it could help me, so I tried the following instead
console.log(compiledElement.children().scope().vm);
But still it returns as undefined. compiledElement.children().scope() does return a large object with lots of angular $$ prefixed scope related properties and I can see my vm controller I'm trying to get at is buried deep within, but not sure this is the right approach
UPDATE 3
I have come across this article which covers exactly the kind of thing I'm trying to achieve.
When I try to implement this approach in my test, I can get to the element of the child directive, but still I am unable to retrieve it's scope:
beforeEach(function(){
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
compiledElement = $compile(element)($scope);
$scope.$digest();
element = element.find('act-table-row');
console.log(element);
console.log(element.scope()); //returns undefined
});
I just wonder if this is down to me using both a link function and controllerAs syntax?
You were very close with the original code you'd posted. I think you were just using .controller('myTableRow') on the wrong element, as your compiledElement at this point was the whole table element. You needed to get a hold of the actual tr child element in order to get the myTableRow controller out of it.
See below, specifically:
controller = compiledElement.find('tr').controller('myTableRow');
/* Angular App */
(function() {
"use strict";
angular
.module('app.components', [])
.directive('myTableRow', myTableRow);
function myTableRow() {
return {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
function link($scope, $element, $attrs, $ctrls) {
var self = $ctrls.shift(),
tableCtrl = $ctrls.shift();
self.toggle = function() {
// keeping it simple for the unit test...
tableCtrl.selected[0] = self.model;
};
}
function controller() {}
}
})();
/* Unit Test */
(function() {
"use strict";
describe('myTableRow Directive', function() {
var $compile,
$scope,
compiledElement,
tableCtrl = {},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
tableCtrl.enableMultiSelect = true;
tableCtrl.selected = [];
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
$scope.data = {
foo: 'bar'
};
compiledElement = $compile(element)($scope);
$scope.$digest();
controller = compiledElement.find('tr').controller('myTableRow');
//console.log(controller); // without the above .find('tr'), this is undefined
});
describe('select', function() {
it('should work', function() {
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
})();
<link rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular-mocks.js"></script>
Here is an example to quote the use of angular directives using the parent child relationship.
The definition of annotated-image looks like this:(which is the parent)
angular.module('annotatedimage').directive('annotatedImage', function() {
function AnnotatedImageController(scope) {}
return {
{
restrict: 'E',
template: [
'<annotated-image-controls annotations="configuration.annotations"></annotated-image-controls>',
'<annotated-image-viewer src="configuration.image" annotations="configuration.annotations"></annotated-image-viewer>',
'<annotated-image-current></annotated-image-current>'
].join('\n'),
controller: ['$scope', AnnotatedImageController],
scope: {
configuration: '='
}
}
};
});
Now for the annotatedImageController , annotatedImageViewer and the annotatedImageCurrent which are the children.
angular.module('annotated-image').directive('annotatedImageControls', function() {
function link(scope, el, attrs, controller) {
scope.showAnnotations = function() {
controller.showAnnotations();
};
controller.onShowAnnotations(function() {
scope.viewing = true;
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: [
'<div>',
'<span span[data-role="show annotations"] ng-click="showAnnotations()" ng-hide="viewing">Show</span>',
'<span span[data-role="hide annotations"] ng-click="hideAnnotations()" ng-show="viewing">Hide</span>',
'<span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>',
'</div>'
].join('\n'),
link: link,
scope: {
annotations: '='
}
};
});
angular.module('annotated-image').directive('annotatedImageViewer', function() {
function link(scope, el, attrs, controller) {
var canvas = el.find('canvas');
var viewManager = new AnnotatedImage.ViewManager(canvas[0], scope.src);
controller.onShowAnnotations(function() {
viewManager.showAnnotations(scope.annotations);
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: '<canvas></canvas>',
link: link,
scope: {
src: '=',
annotations: '='
}
};
});
The same can be done for the annotatedImageCurrent
Summary
<parent-component>
<child-component></child-component>
<another-child-component></another-child-component>
</parent-component>
Parent Component
module.directive('parentComponent', function() {
function ParentComponentController(scope) {
// initialize scope
}
ParentComponentController.prototype.doSomething = function() {
// does nothing here
}
return {
restrict: 'E',
controller: ['$scope', ParentComponentController],
scope: {}
};
});
Child Component
module.directive('childComponent', function() {
function link(scope, element, attrs, controller) {
controller.doSomething();
}
return {
restrict: 'E',
require: '^parentComponent',
link: link,
scope: {}
}
});
Is it possible to have a parent directive and a child directive, both with their own controller?
Something like:
.controller('ParentController', function () {
var self = this;
self.action = function () {
console.log('parent action');
}
})
.controller('TestSomethingController', function () {
var self = this;
self.something = function () {
console.log('something');
}
})
.directive('parent', function () {
return {
restrict: 'A',
controller: 'ParentController',
link: function (scope, element, attrs, controller) {
controller.action();
}
}
})
.directive('test', function () {
return {
restrict: 'A',
require: 'parent',
controller: 'TestSomethingController',
link: function (scope, element, attrs, controller) {
controller.something();
}
};
});
I tried to do this one codepen like this:
http://codepen.io/r3plica/pen/bdygeP?editors=101
If I remove the require, it obviously works, but I would like to keep the require.
Does anyone know if that is possible?
You can require multiple directives. Have it require itself as well as parent. With this syntax, the last parameter of link will be an array of the controllers of the given directives.
.directive('test', function () {
return {
restrict: 'A',
require: ['parent', 'test'],
controller: 'TestSomethingController',
link: function (scope, element, attrs, controllers) {
controllers[0].action(); // parent
controllers[1].something(); // self
}
};
});
Here is a forked, working version of your CodePen.
app.directive('mainCtrl', function () {
return {
controller: function () {
this.funcA = function(){}
}
};
});
app.directive('addProduct', function () {
return {
restrict: 'E',
require: '^mainCtrl',
link: function (scope, lElement, attrs, mainCtrl) {
mainCtrl.funcA()
}
};
});
I don't want to use the link method but the controller method.
Is there a way to get the mainCtrl in the controller method of the directive addProduct.
something like:
app.directive('addProduct', function () {
return {
restrict: 'E',
require: '^mainCtrl',
controller: function (scope, mainCtrl) {
mainCtrl.funcA()
}
};
});
You'd still need to use the link function because the controllers are injected there. What you could, however, is request your directive's own controller and then set the other required controller as its property:
app.directive('addProduct', function () {
return {
restrict: 'E',
require: ['addProduct','^mainCtrl'],
controller: function ($scope) {
// this.mainCtrl is still not set here
// this.mainCtrl.funcA(); // this will cause an error
// but typically it is invoked in response to some event or function call
$scope.doFuncA = function(){
this.mainCtrl.funcA();
}
},
link: function(scope, element, attrs, ctrls){
var me = ctrls[0], mainCtrl = ctrls[1];
me.mainCtrl = mainCtrl;
}
};
});
Since AngularJS 1.5, you can use the $onInit lifecycle hook of the controller. As written in the documentation of require, when defining require as an object and setting bindToController to true, the required controllers are added to the controller as properties after the controller has been constructed, but before the $onInit method is run. So the code would look like this:
app.directive('mainCtrl', function () {
return {
controller: function () {
this.funcA = function(){}
}
};
});
app.directive('addProduct', function () {
return {
restrict: 'E',
require: {
myParentController: '^mainCtrl'
},
bindToController: true,
controller: function ($scope) {
this.$onInit = function() {
this.myParentController.funcA();
};
}
};
});
Here is my solution:
app.directive('mainCtrl', function () {
return {
controllerAs: 'main',
controller: function () {
this.funcA = function(){}
}
};
});
app.directive('addProduct', function () {
return {
restrict: 'E',
require: '^mainCtrl',
controller: function ($scope) {
$scope.main.funcA();
}
};
});
Pass the controller to the scope on the link function then accessing the scope on controller. Like this:
app.directive('mainCtrl', function () {
return {
controller: function () {
this.funcA = function(){}
}
};
});
app.directive('addProduct', function () {
return {
restrict: 'E',
require: '^mainCtrl',
link: function (scope, lElement, attrs, mainCtrl) {
scope.ctrl=mainCtrl;
},controller:function($scope){
$scope.ctrl.funcA();
}
};
});
Assume that I have a directive like this
<div my-directive callback='doSomething(myArg)'></div>
angular.module('directives').directive('myDirective', function() {
return {
restrict: 'A',
scope: {
callback: '&'
},
link: function(scope, element, attrs) {
element.bind('someEvent', function() {
scope.callback({myArg: 'bla'});
});
}
}
});
If I want to pass a parameter to my scope's function, I have to do scope.callback({myArg: 'bla'}). I wonder if there's a way pass the argument without having to specify its name?
Use can use shared service in this case and inject it to directive:
angular.module("yourAppName", []).factory("mySharedService", function($rootScope){
var mySharedService = {};
mySharedService.values = {};
mySharedService.setValues = function(params){
mySharedService.values = params;
$rootScope.$broadcast('dataPassed');
}
return mySharedService;
});
And after inject it to directive. For example:
app.directive('myDirective', ['mySharedService', function(mySharedService){
return {
restrict: 'C',
link: function (scope, element, attrs) {
mySharedService.setValues(//some value//);
}
}
}]);
Then, you can get necessary value in controller.
function MyCtrl($scope, mySharedService) {
$scope.$on('dataPassed', function () {
$scope.newItems = mySharedService.values;
});
}