Pass JSON object from Angular service to directive - javascript

I'm trying to pass a JSON object from an Angular service to a directive. Actually I just want to $compile a directive on-the-go and pass an object to the directive.
It should look something like this:
var template = '<gmap-info-window layer="layer" marker="marker"></gmap-info-window>',
content = $compile(template)(searchScope);
Whereas the directive looks like this:
.directive('gmapInfoWindow', [function() {
scope: {
marker: '=',
layer: '='
},
link: function(scope, element, attrs) {
// access objects in attrs
}
}]);
That doesn't work. All I get in the attrs.marker and attrs.layer is plain strings.
Now what I've tried and accomlished is using the transcludeFn function of the $compile function. It works, but I don't feel it being the right way to do what I'm trying to accomplish.
var template = '<gmap-info-window></gmap-info-window>',
content = $compile(template)(searchScope, null, {
parentBoundTranscludeFn: function() {
return {
marker: _marker,
layer: _layer
};
}
});
Whereas the directive looks like this:
.directive('gmapInfoWindow', [function() {
scope: {},
link: function(scope, element, attrs, controller, transcludeFn) {
var objects = transcludeFn();
// The marker and layer are in objects now!
}
}]);
I can't imagine that there's no other way to do what I wanna do. This looks kinda dirty. Thanks for your insight!

All I get in the attrs.marker and attrs.layer is plain strings.
You need to understand that attribute is always a string by definition. It not possible that you have an object there. What Angular does is it evaluates values of those attributes (strings) in proper context (scope of compilation) according to scope configuration of the directive. Then the result of this evaluation is available in scope object of the link function. This is what you need to use:
link: function(scope, element, attrs) {
console.log(scope.marker, scope.layer);
}

Related

Custom directive scope vs attrs

I have one concern when creating a custom directive in angular.
When I'm using a link function, I'm not sure what is the real difference when accessing attributes with attrs or scope.
Take this piece of code for example:
myApp.directive('someDirective', function() {
return {
restrict: 'E',
replace: true,
scope: {
title: '=title'
},
template: '<img/>',
link: function(scope, element, attrs) {
if (scope.title) {
// do something here
}
if (attrs.title){
// do something here
}
},
}
From my observations accessing 'title' attribute from attrs and by scope has a similar effect. What is the real difference?
The difference is that attribute is of a String type by definition. Always. In your case attrs.title will be literally string equal to whatever you pass into attribute in HTML.
However, scope.title is parsed and evaluated result of the attribute attr.title.
Ex. If you use something like this in HTML
<some-directive title="name"></some-directive>
where $scope.name = "Thomas Mann" defined in the scope, then attr.title will be string "name", while scope.title will be "Thomas Mann".

AngularJS: Indicate the scope of a directive

I am building a directive that can be use in different controllers, and I would like to be able of bind the directive to a particular property of my $scope.
I would like to do something like this:
<div ng-app="myapp">
<div ng-controller="myController">
<my-directive-wrapper ng-model="mymodel">
<my-directive-inner ng-repeat="item in items" />
</my-directive-wrapper>
</div>
</div>
With this model:
$scope.mymodel = {
name : "Transclude test",
items : [
{ title : "test1" },
{ title : "test2" },
{ title : "test3" }
]
};
So the directive myDirectiveWrapper gets $scope.mymodel as scope, and nothing else. Then I may put the directive twice, pointing to a different property each.
I have a demo project with the problem here: http://jsfiddle.net/vtortola/P8JMm/3/
And the same demo working normally (without limiting the scope) here: http://jsfiddle.net/vtortola/P8JMm
The question is, how to indicate in the use of my directive that I want to use a particular property of my $scope as scope of my directive. It should be possible to bind the directive to arbitrary properties in the same $scope.
Cheers.
So the basic answer to this question is - you can do what you want to do, but it is a bit more complicated than you might think. To understand what is happening here you need to know about scopes in angular. A scope is essentially an object that contains the data accessible to the view. There are (at least) three ways scopes operate in angular:
Isolated - In this case angular basically creates a brand new scope for the directive. None of the properties are copied over.
Extended - In this case you would start with the root scope but make a shallow copy of it. Objects that are changed will be changed in the root scope but primitives will not be.
Shared - In this case you share share some or even all of the data with the root scope.
Based on your question above, what you what to do here is to extend the parent scope - copying an object to a property with a specific name in the newly created child scope. The way to get this behavior is to manually create a new child scope before the transclude. The two key lines of code to do this are:
// create a "new" scope
var childScope = scope.$new();
// extend using the model binding provided
angular.extend(childScope, scope[iAttr.myModel]);
In the context of your directive this looks like:
.directive('myDirectiveWrapper', ['$compile', function ($compile) {
return {
transclude: true,
restrict: 'E',
compile: function (element, attrs, transclude) {
var contents = element.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
var childScope = scope.$new();
angular.extend(childScope, scope[iAttr.myModel]);
if (!compiledContents) {
compiledContents = $compile(contents, transclude);
}
compiledContents(childScope, function(clone, childScope) {
iElement.append(clone);
});
};
},
template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
}])
Now you can specify any variable that you want as the "model" for the child scope and you can then access that directly in the contents of your transcluded code!
SEE THE FIDDLE: http://jsfiddle.net/P8JMm/7/
EDIT: Just for fun, I created a more complicated use case for this directive: http://jsfiddle.net/P8JMm/9/
Note - angular site also has some really good resources to understand scope better. See here.
If you want two way binding to work it's going to be a lot easier to just create a variable on your directive scope rather than apply mymodel directly onto the directive scope.
HTML
<div ng-app="myapp">
<div ng-controller="myController">
<my-directive-wrapper model="mymodel">
<my-directive-inner ng-repeat="item in mymodel.items" />
</my-directive-wrapper>
</div>
</div>
Directive
.directive("myDirectiveWrapper", function(){
return {
scope: {
model: '='
},
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, controller) {
},
template: "<div><h3>{{ model.name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
})
http://jsfiddle.net/kQ4TV/
If you don't care about two way binding I suppose you could do something like this but I wouldn't recommend it:
.directive("myDirectiveWrapper", function(){
return {
scope: {
model: '='
},
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, controller) {
angular.extend(scope, scope.model);
},
template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
})
http://jsfiddle.net/vWftR/
Here is an example of when that second approach can cause problems. Notice that when you enter something into the input field it will change the directive's name but not the name in the outer scope: http://jsfiddle.net/r5JeJ/

Getting directive name in AngularJS

I've got an Angular directive. Inside the link function, I do this:
link: function(scope, element, attrs) {
...
element.data('startY', value);
...
}
What I'd like to do is perfix 'startY' with the name of the directive, without hard-coding the name. I'd like to dynamically get the name.
Is there any way to do this? Does Angular provide a way to reflect it? Something like:
link: function(scope, element, attrs) {
...
element.data(this.$name + '-startY', value);
...
}
If not, what are the recommended best practices for choosing data() keys to avoid collisions?
As indicated in the AngularJS source code, a directive's name is assigned in the context of the object literal where your directive options reside. The link function however cannot access the object literal's context, this, because it will be transferred to a compile function where it will be returned and invoked after the compilation process has taken place.
To get the name within your link function you can follow any of these suggestions:
[ 1 ] Create a variable that may hold reference to the object literal(directive options).
.directive('myDirective', function() {
var dir = {
link: function(scope, elem, attr) {
console.log(dir.name);
}
};
return dir;
});
[ 2 ] You can also get the directive's name by using the compile function since it is invoked in the context of the directive option.
.directive('myDirective', function() {
return {
compile: function(tElem, tAttr) {
var dirName = this.name;
return function(scope, elem, attr) { /* link function */ }
}
};
});
As far as I can tell you've answered your own question. You may prefix the name by string concatenation as you've done but it will probably be easier to add it as a separate data store.
element.data('directiveName', this.$name).data('startY', value);
I'm not sure what you mean by avoid collision as this will only apply to the element that was passed into the link function.

Call function on directive parent scope with directive scope argument

I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk

Modify template in directive (dynamically adding another directive)

Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>

Categories