In my AngularJs (v1.5.9) app, I have a kind of list view directive that itself depends on a directive to render the individual, complex items. What exactly should be rendered inside the list items is decided by the consumer and passed in via transclusion.
So the basic structure looks something like this:
<list-directive>
<list-item>
<some more stuff />
<transcluded content />
</list-item>
</list-directive>
I am now trying to add another directive to this structure, that takes data that can be passed in via an attribute from the outside into the top-level element and then does stuff depending on that input for each of the list-items.
The structure is somewhat complex and I tried to reduce the code snippet below to the bare minimum.
// controller
(function () {
'use strict';
function FcDataListCtrl($scope, $q, $element) {
var that = this;
initVars();
init();
function initVars() {
that.actionButtons = that.actionButtons || [];
}
function init() {
}
}
angular
.module('controls.fcDataList.controller', [])
.controller('fcDataListCtrl', FcDataListCtrl);
})();
(function () {
'use strict';
function FcDataList() {
return {
restrict: 'E',
transclude: true,
template: `<fc-data-list-item ng-repeat="item in ::fcDataList.items" item="::item">
<div ng-transclude></div>
</fc-data-list-item>`,
scope: {
items: '=?',
actionButtons: '=?'
},
controller: 'fcDataListCtrl',
controllerAs: 'fcDataList',
bindToController: true
};
}
angular
.module('controls.fcDataList', [
'controls.fcDataList.controller',
'controls.fcDataList.item'
])
.directive('fcDataList', FcDataList);
})();
(function () {
'use strict';
function FcDataListItem() {
return {
restrict: 'E',
replace: true,
require: '^fcDataList',
transclude: 'element',
template: `<div>
<div ng-transclude></div>
<fc-item-menu items="fcDataList.actionButtons"></fc-item-menu>
</div>`,
scope: {
item: '=?'
},
link: {
pre: FcDataListItemLink
}
};
function FcDataListItemLink(scope, elem, attrs, fcDataListCtrl) {
initVars();
init();
function initVars() {
}
function init() {
console.log('FcDataListItem')
console.dir(fcDataListCtrl.actionButtons);
}
}
}
angular
.module('controls.fcDataList.item', [
'components.fioControlsExtensions.fcDataList.menu'
])
.directive('fcDataListItem', FcDataListItem);
})();
(function () {
'use strict';
function FcItemMenu() {
return {
restrict: 'E',
template: `<div ng-repeat="item in items">
<div>{{ item.icon }}</div>
</div>`,
scope: {
items: '=?'
},
link: {
pre: FcItemMenuLink
}
};
function FcItemMenuLink(scope, elem, attrs) {
scope.open = open;
initVars();
init();
function initVars() {
console.log('MenuItem');
console.dir(scope.items);
}
function init() {
}
function open(event) {
}
}
}
angular
.module('components.fioControlsExtensions.fcDataList.menu', [])
.directive('fcItemMenu', FcItemMenu);
})();
(function () {
'use strict';
function AppCtrl() {
var that = this;
init();
function init() {
that.fcDataList = {
buttons: [
{ icon: 'ff-show' }
],
items: [
{ firstName: 'Ivan', lastName: 'Petrov', jobPosition: 'Zookeeper' },
{ firstName: 'Andrei', lastName: 'Müller', jobPosition: 'Pilot' },
{ firstName: 'Christian', lastName: 'Klein', jobPosition: 'Cook' },
{ firstName: 'Peter', lastName: 'Stoyanov', jobPosition: 'Fuller' },
{ firstName: 'Nadine', lastName: 'Wolf', jobPosition: 'Driving Instructor' },
{ firstName: 'Amya', lastName: 'Krüger', jobPosition: 'Military' }
],
}
}
}
angular
.module('controls.example', [
'controls.fcDataList'
])
.controller('AppCtrl', AppCtrl)
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<html lang="en" ng-app="controls.example">
<body ng-controller="AppCtrl as app">
<b>Test for data list n stuff</b>
<fc-data-list items="app.fcDataList.items"
action-buttons="app.fcDataList.buttons">
<div class="row">
<span> This is a list item </span>
</div>
</fc-data-list>
</body>
</html>
here is alsow a Codepen with the same example: https://codepen.io/lyioth/pen/LbqWLz/
Please note that the transclusion of the actual item content is not shown here (but that is working without a problem, so I skipped it).
The actual problem is that, the items in fc-item-menu stay undefined. I added some log statements to show that at the levels above this component the array in question is not in fact empty.
If I change the directive to also require the controller and access the actionButtons property directly, it seems to work. But I'd rather not do that.
So the question is, why doesn't this work as expected? What am I missing?
The items at fc-item-menu are bound to fcDataList.actionButtons. That means that the fc-item-menu directive will look for them at scope.fcDataList.actionButtons.
Let's go one step back - in your fc-data-list directive you have the same type of binding - fcDataList.item. That works, because you have used bindToController and controllerAs, which puts the isolated scope in a scope field, with the name defined in controllerAs (scope.fcDataList).
Back to fc-item-menu: although you have require: ^fcDataList in the directive, the above expression will be undefined, because no scope.fcDataList exist. In addition that is true, because with the transclusion, an isolated scope has been created, which does not carry the scope.fcDataList. The way to make it work is define a scope property yourself with the values from the controller, like so:
function FcDataListItemLink(scope, elem, attrs, fcDataListCtrl) {
scope.buttons = fcDataListCtrl.actionButtons;
}
Here's a working plnkr: https://codepen.io/anon/pen/GNzELy
Related
How do I make a directive run a function like the other built in directives in angular?
In example:
<div ng-repeat="someId in myList" my-directive="runFunctionInScope(someId, divHtmlElement)" />
myApp.directive('myDirective', function ()
{
return function (scope, element, attrs)
{
//??
}
}
You can try something like the below code snippet. Also please check this plunker for working example of your given scenario.
Template:
<div ng-repeat="someId in myList" my-method='theMethodToBeCalled' my-id='someId.id' />
Controller:
app.controller('MainCtrl', function($scope) {
$scope.myList=[{
id: 1,
value: 'One'
}, {
id: 2,
value: 'Two'
}];
$scope.theMethodToBeCalled = function(id) {
alert(id);
};
});
Directive:
app.directive("myMethod",function($parse) {
var directiveDefinitionObject = {
restrict: 'A',
scope: {
method:'&myMethod',
id: '=myId'
},
link: function(scope,element,attrs) {
var expressionHandler = scope.method();
expressionHandler(scope.id);
}
};
return directiveDefinitionObject;
});
In angularjs I have been trying to access main controller $scope variable in my directive isolated scope.
My html code,
<body ng-controller="MainCtrl">
<div id="TestContainer" class="TestContainer" ng-init=Intialfunc()>
<collection collection='testdata'>{{testdata}}</collection>
</div>
</body>
My directive code,
var app = angular.module('plunker', []);
app.directive('collection', function () {
return {
restrict: "E",
replace: true,
scope: {collection: '='},
//controller: 'TreeController',
//bindToController: true,
template: "<ul><member ng-repeat='member in collection' member='member'></member></ul>"
}
})
app.directive('member', function ($compile) {
var linkerfunc = function(scope, element, attrs) {
var collectionSt = '<collection collection="member.children"></collection>';
$compile(collectionSt)(scope, function(cloned, scope) {
element.append(cloned);
});
}
return {
restrict: "E",
replace: true,
scope: {member: '=', ShowDetailsCtrlFunc : '&'},
template: "<li><span ng-click=ShowDetailsCtrlFunc()>{{member.NodeName}}</span></li>",
controller: 'MainCtrl',
//controllerAs: 'MainCtrl',
//bindToController: true,
link: linkerfunc
}
})
My controller code,
app.controller('MainCtrl', function ($scope) {
$scope.Intialfunc = function() {
$scope.testdata = []
var myjsondata = JSON.parse('{ "NodeName": "Parent", "children": [ { "NodeName": "mychild", "children": [ { "NodeName": "chld1", "children": [] } ] } ] }');
$scope.testdata.push(myjsondata);
console.log($scope.testdata) //This one is showing
}
$scope.ShowDetailsCtrlFunc = function(element,event) {
console.log("in function ShowDetailsCtrlFunc"); // coming to this fucntion on click.
console.log($scope.testdata) // but this one is not showing . shows undefined.
//event.stopImmediatePropagation();
};
});
it is coming to the function but not showing the controller $scope. I have created a plunker ,
plunker
Please help me. I have been struggling for many days.
You need to add a function expression to both of your directives' isolate scopes in order to properly call a function in your parent scope. Taking your original code, it should look something like this:
var app = angular.module('plunker', []);
app.directive('collection', function () {
return {
restrict: "E",
//replace: true, <- this is deprecated and should no longer be used
scope: {
collection: '=',
onMemberClick: '&'
},
template: "<ul><member ng-repeat='member in collection' member='member' on-click='onMemberClick()'></member></ul>"
}
})
app.directive('member', function ($compile) {
return {
restrict: "E",
//replace: true, <- this is deprecated and should no longer be used
scope: {
member: '=',
onClick : '&'
},
template: "<li><span ng-click='onClick()'>{{member.NodeName}}</span></li>"
}
});
And you original html should look something like this:
<body ng-controller="MainCtrl">
<div id="TestContainer" class="TestContainer" ng-init=Intialfunc()>
<collection collection='testdata' on-member-click='ShowDetailsCtrlFunc ()'>{{testdata}}</collection>
</div>
</body>
Argument binding
If you would like to actually know which member was clicked, you'll need to bind arguments to your function calls.
var app = angular.module('plunker', []);
app.directive('collection', function () {
return {
restrict: "E",
scope: {
collection: '=',
onMemberClick: '&'
},
template: "<ul><member ng-repeat='member in collection' member='member' on-click='onMemberClick({member: member})'></member></ul>"
}
})
app.directive('member', function ($compile) {
return {
restrict: "E",
scope: {
member: '=',
onClick : '&'
},
template: "<li><span ng-click='onClick({member: member})'>{{member.NodeName}}</span></li>"
}
});
Html:
<body ng-controller="MainCtrl">
<div id="TestContainer" class="TestContainer" ng-init=Intialfunc()>
<collection collection='testdata' on-member-click='ShowDetailsCtrlFunc (member)'>{{testdata}}</collection>
</div>
</body>
MainCtrl:
app.controller('MainCtrl', function ($scope) {
$scope.Intialfunc = function() {
$scope.testdata = []
var myjsondata = JSON.parse('{ "NodeName": "Parent", "children": [ { "NodeName": "mychild", "children": [ { "NodeName": "chld1", "children": [] } ] } ] }');
$scope.testdata.push(myjsondata);
console.log($scope.testdata) //This one is showing
}
$scope.ShowDetailsCtrlFunc = function(member) {
console.log("In show details function");
console.log(member);
};
});
plunker
Lets Begin with the query you have. You want to call a function from link inside the directive even when the scope is isolated. It's simple you want to access parent scope.
Here's the code you can use to access parent scope.
scope.$parent.yourFun();
//or you can do this by the code give below.
//Inside Directive Use this.
scope:{
fun:"&"
},
//now you can call this function inside link
link:function(scope, element,attr){
scope.fun();
}
In your app.directive, just put scope : false.
Your directive will use the same scope as his parent scope.
I have a little SPA using angular. The concept is simple, after login, $routeProvider redirects to a home page where I have a homeController specified.
this is from my home view that is rendered by ng-view while navigating to "/home" :
<my-directive datas=getData()></my-directive>
<ul>
<li ng-repeat="data in datas"> {{data.title}} {{data.content}} </li>
</ul>
my directive is written as:
angular.module('app').directive('myDirective', ['myService', function (myService) {
return {
restrict: "E",
scope: {
data: '='
},
templateUrl: "partials/my-directive.html",
controller: function ($scope) {
$scope.getDatas = function()
{
myService.retData();
}
}
};
}]);
the home controller is:
angular.module('app').controller('homeController', homeController);
homeController.$inject = ['myService', '$scope'];
function homeController(myService, $scope) {
var vm = this;
vm.data = [];
initController();
function initController() {
vm.data = myService.retData();
}
}
and finally my service is
angular.module('app').service('myService', myService);
function myService () {
var data = [
{ id: 1, title: 'Albert Einstein', content: 'Random Content' }
];
return {
retData: function () {
return data;
},
addData: function (title, content) {
var currentIndex = data.length + 1;
data.push({
id: currentIndex,
title: title,
content: content
});
}
};
}
now that i mentioned everything, here comes the problem. the directive is not able to retrieve data from the service. Actually when i run the project in VS2013, myDirective.js is not even loaded. I included all services, directives, controllers etc in the main HTML page.
What is causing this problem?
Does it have something to do with the scope being isolated in the directive?
What is a better approach to sharing data between a controller, directive and service?
I may have made some silly mistakes while rewriting all the code. Please do point them out, however keep in mind my actual issue and what error may be causing that.
Better to use isolated scope to pass data controller to directive.
Html:
<my-directive datas="getData()" data="data"></my-directive>
Directive:
angular.module('app').directive('myDirective', [function () {
return {
restrict: "E",
scope: {
data: '='
},
templateUrl: "partials/my-directive.html",
link: function (scope) {
//Here you got the isolated scope data
var details = scope.data;
}
};
}]);
OR
app.directive('myDirective', function() {
return {
restrict: 'E',
templateUrl: 'partials/my-directive.html',
scope: {
date: '=',
},
controller : ['$scope', 'myService', function($scope, myService) {
myService.retData();
}],
link: function(scope, elem, attr) {
//
}
};
});
I'm a bit stuck on an directive which add attributes and recompile the element.
If I had a scope on the directive ng-change is not triggered anymore (without it it works). I based my test on this answer
The HTML
<div ng-app="myApp">
<div ng-controller='testController'>
<div ng-repeat="field in fields">
<input type="text" ng-model="ngModel[field.fieldName]" property="{{formParams.getProperties(field.fieldName)}}" update-attr ng-change="test()" />
</div>
</div>
</div>
The directive:
angular.module('myApp', [])
.controller('testController', function ($scope) {
$scope.properties = {
"something": {
"style": "float:left;"
},
"something2": {
"style": "float:right;"
}
};
$scope.ngModel = {};
$scope.fields = [{
fieldName: 'something'
}, {
fieldName: 'something2'
}];
$scope.test = function () {
alert('i dont get triggered');
};
$scope.formParams = {
getProperties: function (fieldName) {
return $scope.properties[fieldName];
}
};
})
.directive('updateAttr', function ($compile) {
return {
restrict: 'A',
replace: true,
terminate: true,
scope: {
ngModel : '='
},
link: function (scope, elem, attrs) {
if (angular.isDefined(attrs['property']) && attrs['property'].lenght != 0) {
var json = JSON.parse(attrs['property']);
angular.forEach(json, function (value, key) {
elem.attr(key, value);
});
elem.removeAttr('property');
var $e = $compile(elem[0].outerHTML)(scope);
elem.replaceWith($e);
}
}
};
});
Here a fork of the fiddle to test with a scope on the directive: fiddle
Do you have any suggestion ?
I found why ng-change was not trigger so I share the answer:
When we add scope attribute on the directive, a new scope is created. So we have to use $scope.$parent for the compilation. I have updated the fiddle with the correction.
I have 2 directives both placed on a single div. But only the content of the first directive is placed inside the div. Is it possible to let 2 directives change the html of this div?
-HTML
<div messages messageControls/>
-Messages Directive
(function() {
"use strict";
define(function() {
messages = [
function() {
return {
priority: 1,
restrict: 'A',
templateUrl: '../../messages.html', (<p>hello world</p>)
scope: {
},
link: function(scope) {}
};
}
];
return messages;
});
}).call(this);
-MessageControls Directive
(function() {
"use strict";
define(function() {
messageControls = [
function() {
return {
priority: 2,
restrict: 'A',
templateUrl: '../../messageControls.html', (<p>delete message</p>)
scope: {
},
link: function(scope) {}
};
}
];
return messageControls;
});
}).call(this);
When I only add one directive, it works. But when I add the second one I get the following angular error:"Multiple directives asking for template on