I'm pretty new to Angular and I got a question regarding directives and inheritance.
Let's assume there are two directives:
<my-pic my-log="log pic" my-warn="pic warn" my-src="/some/url/pic.jpg"></my-pic>
<my-button my-log="log button" my-warn="button warn" my-text="button text"></my-button>
JS-Code
angular.module("app", [])
.directive("myPic",function(){
return {
restrict: "E"
,scope: {
log: "#myLog"
,src: "#mySrc"
,warn:"#myWarn"
}
,template: '<img src="{{src}}" title="{{log}} | {{warn}}"></img>'
,controller: function($scope, $log){
$log.log($scope.log);
$log.warn($scope.warn);
}
};
})
.directive("myButton",function(){
return {
restrict: "E"
,scope: {
log: "#myLog"
,text: "#myText"
,warn: "#myWarn"
}
,template: '<button title="{{log}} | {{warn}}">{{text}}</button>'
,controller: function($scope, $log){
$log.log($scope.log);
$log.warn($scope.warn);
}
};
});
Code Pen
http://codepen.io/anon/pen/VLMdRL
QUESTION
The question is, is it possible to create a third directive from which the MyPic and MyButton directives could be derived from and that handles the my-log="..." and my-warn="..." attributes (the $log code and adding the title attribute to the template code)?
The my-log and my-warn attributes must be handled by the same directive and the attribute values should still be accessible by the child directives.
Thanks for your help!
In this case you don't need to inheritance. Just write a service to handle your common logging operations and inject it into your directives.
Okay I found a solution I can live with, however maybe there is a better solution to do this. What I did is, I created a third directive 'myLogger' that is then added to the template of the 'myButton' and 'myPic' directives. I also created a function that is defined as a constant and that adds the logging properties to the scope of the directive. The 'myLogger' directive does the console logging and ads the 'title' attribute to the HTML-element.
Any advices/ideas to improve that solution are very welcome. I would also appreciate if someone has a better solution.
HTML code:
<my-pic my-log="log pic" my-warn="pic warn" my-src="/some/url/pic.jpg"></my-pic>
<my-button my-log="log button" my-warn="button warn" my-text="button text"></my-button>
Javascript code:
angular.module("app", [])
.constant("addLogScope",function(opts){
return angular.extend(opts || {},{
myWarn: "#"
,myLog: "#"
});
})
.directive("myLogger",function(){
return {
restrict: "A"
,scope: true
,controller: function($scope, $log){
console.log($scope.myLog);
console.info($scope.myWarn);
}
,link: function($scope, $element, $attrs){
$element.prop("title",$scope.myLog + " | " + $scope.myWarn);
}
};
})
.directive("myPic",function(addLogScope){
return {
restrict: "E"
,scope: addLogScope({
mySrc: "#"
})
,transclude: true
,template: '<img src="{{mySrc}}" my-logger></img>'
,controller: function($scope){
}
};
})
.directive("myButton",function(addLogScope){
return {
restrict: "E"
,scope: addLogScope({
myText: "#"
})
,template: '<button my-logger>{{myText}}</button>'
,controller: function($scope){
}
};
});
Code pen: http://codepen.io/anon/pen/rVYgew
Related
I'm trying to dynamically add directive names to my directive from a json object. Angular however is only interpolating the directive name which is pulled from a JSON tree once, Angular is then not recognizing and compiling the dynamic children directives once the name is interpolated.
I have tried adding the interpolate service to my DDO so that I can manually interpolate the JSON values, and then have Angular compile.
I however get undefined for $interpolate(tAttrs.$attr.layout) I'm passing the json object to my isolated scope as layout, when I try to access the attr layout I get undefined. My question is how can I access layout object values in the pre link or before compile so that I can interpolate the values and inject them in.
Or do I need to have angular recompile as described here: How do I pass multiple attributes into an Angular.js attribute directive?
Any help would be great.
{
"containers": [
{
"fluid": true,
"rows": [
{
"columns": [
{
"class": "col-md-12",
"directive": "blog"
}
]
},
{
"columns": [
{
"class": "col-md-6 col-md-offset-3 col-xs-10 col-xs-offset-1",
"directive": "tire-finder"
}
]
}
]
}
]
}
...
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<{{column.directive}}></{{column.directive}}>
</div>
</div>
</div>
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate) {
var controller = function(brainService, $scope, $state) {
$scope.directive = {};
var currentState = $state.current.name;
brainService.getDirectiveScope('wpMain', {}).then(function(response) {
$scope.layout = response.states[currentState];
});
};
var compile = function(tElement, tAttrs, transclude) {
var directiveNames = $interpolate(tAttrs.$attr.layout);
}
return {
restrict: 'E',
// replace: true,
scope: {
layout: '=',
},
controller: controller,
templateUrl: 'directive/wpMain/wpMain.html',
compile: compile
};
};
If you're only dealing with a couple options for what a column might be, I would suggest going with #georgeawg's answer.
However, if you expect that number to grow, what you might opt for instead is something along the following lines:
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<column-directive type="column.directive"></column-directive>
</div>
</div>
and then in your JS...
yourApp.directive('columnDirective', columnDirectiveFactory);
columnDirectiveFactory.$inject = ['$compile'];
function columnDirectiveFactory ($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: function (scope, elem, attrs) {
var newContents = $compile('<' + scope.type + '></' + scope.type + '>')(scope);
elem.contents(newContents);
}
};
}
To the best of my knowledge, Angular doesn't have any built-in facility to choose directives in a truly dynamic fashion. The solution above allows you to pass information about which directive you want into a generic columnDirective, whose link function then goes about the business of constructing the correct element, compiling it against the current scope, and inserting into the DOM.
There was an issue with the promise in my original posted code which was preventing me from recompiling the template with the correct directive names. The issue was that I was trying to access the JSON object in the preLink function, but the promise hadn't been resolved yet. This meant that my scope property didn't yet have data.
To fix this I added my service promise to the directive scope $scope.layoutPromise = brainService.getDirectiveScope('wpMain', {}); to which I then called and resolved in my link function. I managed to have Angular compile all of my directive names from the JSON object, but I had to do it in a very hackish way. I will be taking your recommendations #cmw in order to make my code simpler and more 'Angulary'
This is currently my working code:
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate, $compile) {
var controller = function(brainService, $scope, $state) {
$scope.currentState = $state.current.name;
$scope.layoutPromise = brainService.getDirectiveScope('wpMain', {});
};
var link = function(scope, element, attributes) {
scope.layoutPromise.then(function sucess(response) {
var template = [];
angular.forEach(response.states[scope.currentState].containers, function(container, containerKey) {
template.push('<div class="container' + (container.fluid?'-fluid':'') + '">');
//loop rows
angular.forEach(container.rows, function(row, rowkey) {
template.push('<div class="row">');
angular.forEach(row.columns, function(column, columnKey) {
template.push('<div class="' + column.class + '">');
template.push('<' + column.directive +'></' + column.directive + '>')
template.push('</div>');
});
template.push('</div>');
});
template.push('</div>');
});
template = template.join('');
element.append($compile(template)(scope));
})
};
return {
scope: true,
controller: controller,
link: link
};
};
I have a directive that creates a UI that allows the user to perform a search. The directive wraps content which will transclude and become the template for each individual search result. Something like this:
<search>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
I'd like the ng-click to call the selectResult function on the controller's scope, but have the result object come from the directive. How can I accomplish this with an isolated scope in the directive?
Instead of using ng-transclude, you can build your own search transclude directive that can be used to put result onto the transcluded scope. For example, your search directive might look something like this with ng-repeat and the search-transclude directive where you want the transcluded content:
.directive("search", function (SearchResults) {
return {
restrict: "AE",
transclude: true,
scope: {},
template: '<div ng-repeat="result in results">Search Relevance:' +
'{{result.relevance}}' +
//the most important part search-transclude that receives the current
//result of ng-repeat
'<div search-transclude result="result"></div></div>',
link: function (scope, elem, attrs) {
//get search results
scope.results = SearchResults.results;
}
}
})
Build search transclude directive as follows:
.directive("searchTransclude", function () {
return {
restrict: "A",
link: function (scope, elem, attrs, ctrl, $transclude) {
//create a new scope that inherits from the parent of the
//search directive ($parent.$parent) so that result can be used with other
//items within that scope (e.g. selectResult)
var newScope = scope.$parent.$parent.$new();
//put result from isolate to be available to transcluded content
newScope.result = scope.$eval(attrs.result);
$transclude(newScope, function (clone) {
elem.append(clone);
});
}
}
})
The transcluded content will now be able to see selectResult function if it exists in the scope where the search directive was created. Example here.
Transcluded content will always use the scope in which the directive element resides, i.e. your controller scope. That's why if you want the result argument of selectResult function to get it's value from isolated scope, then you need to establish two way binding between isolated scope's and controller scope's result properties. After setting the result property to desired value in isolated scope the controller's scope result property will be updated to the same value. So , transcluded content will use controller's result which is in sync with isolated scope's result.
1) add resultAttr='result' attribute to directive element.
<search resultAttr='result'>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
2) establish two-way binding for result property when you define isolated scope in directive:
scope: {
result: "=resultAttr"
}
3) set result to some value in directive
I'd like the ng-click [in the directive] to call the selectResult
function on the controller's scope...
To pass functions (or properties) into an isolate scope, you use attributes on the directive tag.
...but have the result object come from the directive [scope].
If you want the contents of a directive tag to have access to the directive's scope, you DON'T use transclude. Specifying transclude: true tells angular NOT to allow the contents of a directive tag to have access to the directive's scope--the opposite of what you want.
To accomplish #1, you could make the user specify the template like this:
<div ng-controller="MainCtrl">
<search external-func='selectResult'>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
</div>
Note, the user needs to add an extra attribute to the <search> tag. Yet, that html may comport better with angular's philosophy that the html should give hints to the developer about what javascript will operate on the elements.
Then you specify the isolate scope like this:
scope: {
selectResult: '=externalFunc'
},
To accomplish #2, don't specify transclude: true in the directive:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope', function($scope) {
$scope.selectResult = function(result) {
console.log("In MainCtrl: " + result.Name);
};
}]);
app.controller('DirectiveCtrl', ['$scope', function($scope) {
$scope.results = [
{Name: "Mr. Result"},
{Name: "Mrs. Result"}
]
}]);
app.directive('search', function() {
return {
restrict: 'E',
scope: {
selectResult: '=externalFunc'
},
template: function(element, attrs) {
// ^ ^
// | |
// directive tag --+ +-- directive tag's attributes
var inner_div = element.children();
inner_div.attr('ng-repeat', 'result in results')
//console.log("Inside template func: " + element.html());
return element.html(); //Must return a string. The return value replaces the innerHTML of the directive tag.
},
controller: 'DirectiveCtrl'
}
}]);
The html could provide an even better record of what the javascript does if you make the user specify their template in more detail:
<search external-func='selectResult'>
<div class="someStyle"
ng-click="selectResult(result)"
ng-repeat="result in results">{{result.Name}}
</div>
</search>
But if you insist on the minimalist html:
<search>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
...then you can dynamically add the ng-repeat attribute(as shown above), and it's also possible to dynamically map an external function to the isolate scope:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope', function($scope) {
$scope.selectDog = function(result) {
console.log("In MainCtrl: you clicked " + result.Name);
};
$scope.greet = function(result) {
console.log('MainCtrl: ' + result.Name);
};
}]);
app.controller('DirectiveCtrl', ['$scope', function($scope) {
$scope.results = [
{Name: "Mr. Result"},
{Name: "Mrs. Result"}
]
}]);
app.directive('search', function() {
return {
restrict: 'E',
scope: {
externalFunc: '&externalFunc' //Cannot write => externalFunc: '&'
}, //because the attribute name is
//'external-func', which means
//the left hand side would have to be external-func.
template: function(element, attrs) {
//Retrieve function specified by ng-click:
var inner_div = element.children();
var ng_click_val = inner_div.attr('ng-click'); //==>"selectResult(result)"
//Add the outer_scope<==>inner_scope mapping to the directive tag:
//element.attr('external', ng_click_val); //=> No worky! Angular does not create the mapping.
//But this works:
attrs.$set('externalFunc', ng_click_val) //=> external-func="selectResult(result)"
//attrs.$set('external-func', ng_click_val); //=> No worky!
//Change ng-click val to use the correct call format:
var func_args = ng_click_val.substring(ng_click_val.indexOf('(')); //=> (result)
func_args = func_args.replace(/[\(]([^\)]*)[\)]/, "({$1: $1})"); //=> ({result: result})
inner_div.attr('ng-click', 'externalFunc' + func_args); //=> ng-click="externalFunc({result: result})"
//Dynamically add an ng-repeat attribute:
inner_div.attr('ng-repeat', 'result in results')
console.log("Template: " + element[0].outerHTML);
return element.html();
},
controller: 'DirectiveCtrl'
}
})
If you want to call the external function with more than one argument, you can do this:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope', function($scope) {
$scope.selectResult = function(result, index) {
console.log("In MainCtrl: you clicked "
+ result.Name
+ " "
+ index);
};
}]);
app.controller('DirectiveCtrl', ['$scope', function($scope) {
$scope.results = [
{Name: "Mr. Result"},
{Name: "Mrs. Result"}
]
}]);
app.directive('search', function() {
return {
restrict: 'E',
scope: {
external: '='
},
template: function(element, attrs) {
//Extract function name specified by ng-click:
var inner_div = element.children();
var ng_click_val = inner_div.attr('ng-click'); //=>"selectResult(result, $index)"
var external_func_name = ng_click_val.substring(0, ng_click_val.indexOf('(') ); //=> selectResult
external_func_name = external_func_name.trim();
//Add the outer_scope<==>inner_scope mapping to the directive tag:
//element.attr('externalFunc', ng_click_val); => No worky!
attrs.$set('external', external_func_name); //=> external="selectResult"
//Change name of ng-click function to 'external':
ng_click_val = ng_click_val.replace(/[^(]+/, 'external');
inner_div.attr('ng-click', ng_click_val);
//Dynamically add ng-repeat to div:
inner_div.attr('ng-repeat', 'result in results');
console.log("Template: " + element[0].outerHTML);
return element.html();
},
controller: 'DirectiveCtrl'
}
});
I'm trying to call a function passed from a controller's scope into a directive via the "&" operation from the directive's controller. That method, however, is claimed by Angular to be undefined. After reading my code over and over, scouring the internet, and then repeating that process, I've decided to turn to help here.
Here's the relevant part of my controller. It contains the method I pass to my directive.
angular.module('myApp.controllers', []).controller('PostCtrl', ['$scope', 'postalService', function($scope, postalService) {
$scope.posts = [];
$scope.getPosts = function() {
postalService.getPosts(function(err, posts) {
if(err);
else $scope.posts = posts;
});
};
}]);
Here's my directive. I am unable to invoke onPost.
angular.module('myApp.directives', []).directive('compose', ['postalService', function(postalService) {
return {
restrict: 'E',
transclude: false,
replace: true,
scope: {
onPost: "&" //why will it not
},
templateUrl: "partials/components/compose-partial.html",
controller: function($scope, postalService) {
$scope.title = "";
$scope.content = "";
$scope.newPost = function() {
postalService.newPost($scope.title, $scope.content, function(err) {
if(err) console.log(err + ":(");
else {
console.log("Success getting posts.");
//why can I not invoke onPost()??
$scope.onPost();
}
});
};
},
};
}]);
And here's the relevant part of my html
<div ng-controller="PostCtrl">
<section class="side-bar panel hide-for-small">
<compose onPost="getPosts()"></compose>
</section>
<!--more, non-relevant html here-->
</div>
I know the problem is not with my postalService Service. Instead, the directive reports that no function is passed to it. Why??
Replace
<compose onPost="getPosts()"></compose>
with
<compose on-post="getPosts()"></compose>
and it'll work.
The Angular docs say why it's so:
Directives have camel cased names such as ngBind. The directive can be
invoked by translating the camel case name into snake case with these
special characters :, -, or _.
When I generate a new element through a string that has a directive (that's why I need to compile) and that directive generates an association with a variable in the controller scope through "=", the variable in my controller isn't associated to the one in the directive.
I created a jsfiddle to show the example where the "door" ng-model value should be associated to all the directives model values.
See this fiddle: http://jsfiddle.net/aVJqU/2/
Another thing I notice is that the directive that run from elements present in the html show the correct association through the variables (controller and directive).
The html (there is the directive that binds <door>):
<body ng-app="animateApp">
<div ng-controller="tst">
<h2> Controller with its model </h2>
<input ng-model="doorval" type="text"> </input>
{{doorval}}
<h2> Directive render directly from the html </h2>
<door doorvalue="doorval"></door> <key></key>
<h2> Directives that are compiled </h2>
<list-actions actions="actions"></list-actions>
</div>
</body>
This is the directive:
animateAppModule.directive('door', function () {
return {
restrict: "E",
scope: {
doorvalue:"="
},
template: '<span>Open the door <input type="text" ng-model="doorvalue"> </input> {{doorvalue}}</span>',
replace: true
}
})
This is the controller:
var animateAppModule = angular.module('animateApp', [])
animateAppModule.controller('tst', function ($scope, tmplService) {
$scope.doorval = "open"
$scope.actions = tmplService;
})
animateAppModule.service('tmplService', function () {
return [{
form_layout: '<door doorvalue="doorval"></door> <key></key>'
}, {
form_layout: '<door doorvalue="doorval"></door> with this <key></key>'
}]
})
And finally this is the directive that compiles the string that has the directive that doesn't bind:
animateAppModule.directive('listActions', function ($compile) {
return {
restrict: "E",
replace: true,
template: '<ul></ul>',
scope: {
actions: '='
},
link: function (scope, iElement, iAttrs) {
scope.$watch('actions', function (neww, old,scope) {
var _actions = scope.actions;
for (var i = 0; i < _actions.length; i++) {
//iElement.append('<li>'+ _actions[i].form_layout + '</li>');
//$compile(iElement.contents())(scope)
iElement.append($compile('<li>' + _actions[i].form_layout + '</li>')(scope))
}
})
}
}
})
What can I do to bind all the "door" ng-model values together?
Where is the compiled directive binding to?
You just have to pass the doorval reference down through all directives without skip any one. The problem was the listActions directive didn't had access to doorval in its scope.
Check this out: http://jsfiddle.net/aVJqU/5/
#Danypype is basically correct as the problem occurs due to scope isolation, as explained in the documentation.
An alternative solution is to simply eliminate the scope isolation by removing the scope block from within the directive definition.
Look at this Fiddle, what do I have to change, that the expressions in the template get evaluated using the arguments I defined in the HTML? The SAVE-button should call the blabla()-function of the controller, since I pass it?
var myApp = angular.module('MyApp',[])
myApp.directive('editkeyvalue', function() {
return {
restrict: 'E',
replace: true,
scope: {
accept: "expression"
},
template : '<div><label class="control-label">{{key}}</label>' +
'<label class="control-label">{{key}}</label>' +
'<input type="text" ng-model="value" />'+
'<button type="button" x-ng-click="cancel()">CANCEL</button>' +
'<button type="submit" x-ng-click="save()">SAVE</button></div>',
controller: function($scope, $element, $attrs, $location) {
$scope.save= function() {
$scope.accept();
};
}
}
});
I do not really see through that. Thanks for help!
You can set two way data binding with property: '=' as Roy suggests. So if you want both key and value bound to the local scope you would do
scope: {
key: '=',
value: '='
},
Since you are passing these values, you have access to them in your directive's controller. But in case you want to run a function in the context of the parent scope, which seems to be what you want to do with the accept attribute, then you would need to tell angular like this
scope: {
accept: "&"
}
Now, from your save method you could call the function passed via accept
controller: function($scope, $element, $attrs, $location) {
$scope.save= function() {
$scope.accept()
};
}
Here's a jsfiddle
scope: {
accept: "&"
}
Use lowercase letters for function names, otherwise it doesn't work.
Just a quick note that you dont need the wrapping function save. Just call this in the template:
'<button type="submit" x-ng-click="accept()">SAVE</button></div>',
That transposes the function call and passes the parameters as expected.
This simplifies code and makes it a lot easier to read.