I would like to manipulate the data sent to my directive, ie. I have a myUser directive that displays the user name and is used like this:
<my-user id="25" name="John Doe"></my-user>
I would like it to convert to:
<a ng-click="navTo('/user/25')">John Doe</a>
So I would like to replace any spaces with and set the new location according to the user's id. Here is my directive definition:
angular.module('myApp').directive('myUser', function ($location, $log) {
return {
restrict: 'EA',
scope: {
id: '#',
name: '#'
},
template: '<a ng-click="navTo(\'/users/\' + {{id}})">{{name}}</a>',
controller: function ($scope) {
$scope.name.replace(' ', ' ');
},
link: function (scope, element, attrs, fn) {
scope.name.replace(' ', ' ');
scope.navTo = function (route) {
$log.info('Navigating to ' + route);
$location.path(route);
};
}
};
});
However, the replace does not take place. I assume both the controller and link functions are executed after the template has been rendered.
Also, the navTo function returns the following error:
Error: [$parse:syntax] Syntax Error: Token 'id' is unexpected, expecting [:] at column 21 of the expression [navTo('/users/' + {{id}})] starting at [id}})].
Any ideas on how to solve this would be greatly appreciated.
Just replace your template with below line and it should work
template: '<a ng-click=\'navTo("/users/{{id}}")\'>{{name}}</a>',
ng-click takes an expression and doesn't need interpolation, so you don't need to use {{}}. You can just remove them, like this...
template: '<a ng-click="navTo(\'/users/\' + id)">{{name}}</a>'
Live Demo - Fiddle
Related
I have an AngularJS directive with function parameter, and it works perfectly when I call the directive simply it works, and I want to generalize it.
What I got so far:
.directive('panelBox', function () {
return {
restrict: 'E',
scope: {
values: '=',
calculatefn: '&'
},
templateUrl: '/ProfitCalculator/PanelBox',
controller: function ($scope) {
$scope.calculate=function() {
$scope.calculatefn();
}
}
}
})
in the main scope:
$scope.smartBookValues= {
name:'Smart Book',
text:'Smart book header',
controls:[]
};
and the html:
<panel-box values="smartBookValues" calculateFn="smartBookCalculateFn()"></panel-box>
now I'm trying to bind the values and calculateFn, so I started with calculateFn and did:
$scope.smartBookValues= {
name:'Smart Book',
text:'Smart book header',
controls:[],
calculateFn:'smartBookCalculateFn()'
};
and the html:
<panel-box values="smartBookValues" calculateFn="{{smartBookValues.calculateFn}}"></panel-box>
but i get: [$parse:syntax]
Syntax Error: Token '{' invalid key at column 2 of the expression [{{smartBookValues.calculateFn}}] starting at [{smartBookValues.calculateFn}}].
First, you declare as:
calculatefn: '&' <-- small 'f'
so syntax in html is like:
<panel-box values="smartBookValues" calculatefn="smartBookValues.calculateFn"></panel-box> <-- no need for {{}}, as you passing as reference to scope, not as text
search Google for "how to pass function in to angular directive"
I'm creating directives for a library that customers can use. I need to let the customers create their own templates for a directive and pass the absolute url value of that template into my directives. One of my directives will have another custom directive inside of it, and it's template will be figured out based upon the value of one of the parent directive's attributes. Here's an example:
<parent-dir menu-template="this.html" item-template="that.html"></parent-dir>
I have a template for this directive that looks like this:
<ul style="list: none" ng-repeat="item in menu">
<child-dir template="{{itemTemplate}}"></child-dir>
</ul>
My directives look like this:
angular.module('myApp')
.directive('parentDir', function () {
return {
restrict: 'E',
scope: {
menuTemplate: '#',
itemTemplate: '#',
menuType: '#',
menuName: '#',
menuId: '#',
},
templateUrl: function (element, attrs) {
alert('Scope: ' + attrs.menuTemplate);
return attrs.menuTemplate;
},
controller: function ($scope, $element, $attrs) {
$scope.defaultSubmit = false;
alert('Menu: '+$attrs.menuTemplate);
alert('Item: ' + $attrs.itemTemplate);
$scope.itemTemplate = $attrs.itemTemplate;
if ($attrs.$attr.hasOwnProperty('defaultSubmit')) {
alert('It does');
$scope.defaultSubmit = true;
}
}
};
})
.directive('childDir', function () {
return {
restrict: 'E',
require: '^parentDir',
templateUrl: function (element, attrs) {
alert('Item Template: ' + attrs.template);
return attrs.template;
},
controller: function ($scope, $element, $attrs) {
$scope.job;
alert('Under job: ' + $scope.itemTemplate);
}
};
});
I'm not showing all of the code but this is the main piece of my problem. When I run this, I keep getting undefined for the template on the childDir.
What is the best practice in perpetuating the value of itemTemplate from the parentDir so that the childDir can use it as it's template?
The reason you're running into problems is because the function that generates the templateUrl is running before a scope has been assigned to your directive - something that has to be done before interpolated data can be replaced.
In other words: at the point that the templateUrl function runs, the value of the template attribute is still "{{itemTemplate}}". This will remain the case until the directive's link (preLink to be precise) function runs.
I created a plunker to demonstrate the point here. Be sure to open the console. You'll see that templateUrl runs before both the parent and child linking functions.
So what do you do instead?
Fortunately, angular provides a $templateRequest service which allows you to request the template in the same way it would using templateUrl (it also uses the $templateCache which is handy).
put this code in your link function:
$templateRequest(attrs.template)
.then(function (tplString){
// compile the template then link the result with the scope.
contents = $compile(tplString)(scope);
// Insert the compiled, linked element into the DOM
elem.append(contents);
})
You can then remove any reference to the template in the directive definition object, and this will safely run once the attribute has been interpolated.
(Lots of similar questions/answers, but couldn't find a solution to this)
Trying to created nested directives. The issue is that the inner directives are being placed above the outer directive.
angular.module('mayofest14ClientApp')
.controller('OrderCtrl', ['$scope',
function ($scope) {
$scope.order = {
activities: [
{formattedTime: '2014-03-04', performedBy: 'matt', action: 'Action', comment: 'Some comment'},
],
};
}
])
.directive('orderActivity', [function () {
return {
scope: {
activities: '=',
},
replace:true,
restrict: 'E',
template:
'<div class="order_activity">' +
' <table>' +
' <caption>order_id History</caption>' +
' <thead>' +
' <tr>' +
' <th>Date</th>' +
' </tr>' +
' </thead>' +
' <tbody>' +
' <p ng-repeat="record in activities" record="record">Order activity {{record.action}} (This is where I want to call a nested directive)</p>' +
'' +
' </tbody>' +
' </table>' +
'</div>',
};
}]);
And the HTML,
<order-activity activities="order.activities"></order-activity>
The result is, the p tag with the ng-repeat is appearing before the template in the orderActivity directive.
I've read stuff about using transclude, messing with replace, some people mentioned using $timeout's or $watch's to adjust the order. The latter especially seemed messy and I couldn't find a good example of it.
Essentially, what can I do to get this rendering in the proper order?
Should I build the link function to generate the template, and write in all the 'record in activities' it self and avoid the 'ng-repeat'?
Oh, this is Angular 1.2.16
Thanks.
(Not sure this is a great solution)
The real motivation was to call an inner directive. I had used a p tag in the question just to simplify.
In any case, I seemed to solve this by ensuring that the inner-directive was compiled before the outer directive. I did this by using $compile and $interpolate, and by creating a scope for the inner directive.
My outer directive:
angular.module('mayofest14ClientApp')
.directive('orderActivity', ['$compile', function ($compile) {
var head =
'<div class="order_activity">' +
' <table>' +
' <caption>order_id History</caption>' +
' <thead>' +
' <tr>' +
' <th>Date</th>' +
' <th>Performed By</th>' +
' <th>Action</th>' +
' <th>Comment(s)</th>' +
' </tr>' +
' </thead>' +
' <tbody>\n';
var body = '';
var foot = ' </tbody>' +
' </table>' +
'</div>';
return {
scope: {
activities: '=',
},
restrict: 'E',
link: function (scope, element, attrs) {
for (var i = 0; i<scope.activities.length; i++) {
var itemScope = scope.$new(true);
itemScope.record=scope.activities[i];
body += $compile('<order-activity-item record="record"></order-activity-item>')(itemScope).html();
}
console.log("Compiling order-activity");
element.html(
$compile(head+body+foot)(scope)
);
}
};
}]);
My inner directive:
angular.module('mayofest14ClientApp')
.directive('orderActivityItem', ['$compile', '$interpolate', function ($compile, $interpolate) {
var template = '<tr><td>{{record.date}}</td><td>{{record.performedBy}}</td><td>{{record.action}}</td><td>{{record.comment}}</td></tr>';
return {
scope: {
record: '=',
},
restrict: 'E',
replace: true,
compile: function compile(element, attributes) {
console.log('orderItem (compile)');
return {
pre: function preLink(scope, element, attributes) {
console.log('orderItem (pre-link)');
element.html($interpolate(template)(scope));
},
post: function postLink(scope, element, attributes) {
console.log('orderItem (post-link)');
}
};
},
};
}]);
So the important parts here are that:
- I'm creating a scope, and giving it the record object read by the inner directive
- In the inner directive compile function, it $interpolates the values into the html, and then compiles it
- The compiled and interpolated HTML is returned to the linking of the outer directive
- The outer directive is then built.
I have not yet checked for memory errors or anything, nor do I know yet if doing this will impact performance in a negative way.
The reason I chose outer/inner directives is that I'd like the inner directives to have actions on it (click to modify, etc..), and this is how I would lay out the objects in a proper OO design. Not sure if directives should reflect OO architecture though.
Solutions I saw elsewhere used $watch's and $ons, some even used $timeouts. The latter seems messy and unpredictable, while the former seems odd.. In any case I wasn't really able to get those working.
Problem:
In a directive nested within 3 ui-views, I can't access the values of my isolate scope. scope.values returns {} but when I console.log scope I can see all the values on the values property.
In a different app I can make this works and I converted this one to that method as well but it still doesn't work and I'm tracing the routes, ctrl's and I can't find the difference between the two.
Where I'm trying to access it from
Init App > ui-view > ui-view > ui-view > form-elements > form-accordion-on
What I'm working with:
The view
<ul class='form-elements'>
<li
class='row-fluid'
ng-hide='group.hidden'
ng-repeat='group in main.card.groups'
card='main.card'
form-element
values='values'
group='group'>
</li>
</ul>
This directive contains tons of different form types and calls their respective directives.
.directive('formElement', [function () {
return {
scope: {
values: '=',
group: '='
},
link: function(scope, element) {
l(scope.$parent.values);
element.attr('data-type', scope.group.type);
},
restrict: 'AE',
template: "<label ng-hide='group.type == \"section-break\"'>" +
"{{ group.name }}" +
"<strong ng-if='group.required' style='font-size: 20px;' class='text-error'>*</strong> " +
"<i ng-if='group.hidden' class='icon-eye-close'></i>" +
"</label>" +
"<div ng-switch='group.type'>" +
"<div ng-switch-when='accordion-start' form-accordion-on card='card' values='values' group='group'></div>" +
"<div ng-switch-when='accordion-end' form-accordion-off values='values' class='text-center' group='group'><hr class='mbs mtn'></div>" +
"<div ng-switch-when='address' form-address values='values' group='group'>" +
"</div>"
};
}])
This is the directive an example directive.
.directive('formAccordionOn', ['$timeout', function($timeout) {
return {
scope: {
group: '=',
values: '='
},
template: "<div class='btn-group'>" +
"<button type='button' class='btn' ng-class='{ active: values[group.trackers[0].id] == option }' ng-model='values[group.trackers[0].id]' ng-click='values[group.trackers[0].id] = option; toggleInBetweenElements()' ng-repeat='option in group.trackers[0].dropdown track by $index'>{{ option }}</button>" +
"</div>",
link: function(scope, element) {
console.log(scope) // returns the scope with the values property and it's values.
console.log(scope.values); // returns {}
})
// etc code ...
Closely related to but I'm using = on every isolate scope object:
AngularJS: Can't get a value of variable from ctrl scope into directive
Update
Sorry if this is a bit vague I've been at this for hours trying to figure out a better solution. This is just what I have atm.
Update 2
I cannot believe it was that simple.
var init = false;
scope.$watch('values', function(newVal, oldVal) {
if (_.size(newVal) !== _.size(oldVal)) {
// scope.values has the value I sent with it!
init = true;
getInitValues();
}
});
But this feels hacky, is there a more elegant way of handling this?
Update 3
I attach a flag in my ctrl when the values are ready and when that happens bam!
scope.$watch('values', function(newVal) {
if (newVal.init) {
getInitValues();
}
});
The output of console.log() is a live view (that may depend on the browser though).
When you examine the output of console.log(scope); scope has already been updated in the meantime. What you see are the current values. That is when the link function is executed scope.values is indeed an empty object. Which in turn means that values get updated after the execution of link, obviously.
If your actual problem is not accessing values during the execution of link, the you need to provide more details.
Update
According to your comments and edits you seem to need some one time initialization, as soon as the values are there. I suggest the following:
var init = scope.$watch('values', function(newVal, oldVal) {
if (newVal ==== oldVal) { //Use this if values is replaced, otherwise use a comparison of your choice
getInitValues();
init();
}
});
init() removes the watcher.
I'm passing "/foo/bar/{{value}}" as a string using # to the directive so I can use the interpolate method to construct hrefs for a dropdown. I think angular is looking for value when compiling instead of passing the whole thing as a string. This was working fine in the angular v1.2.0.
The directive code goes like,
return {
templateUrl: '/views/directives/directive-name.html',
restrict: 'E',
replace: true,
scope: {
title: '#',
optHref: '#'
},
link: link: function postLink(scope, element, attrs) {
var hrefFormatter;
hrefFormatter = $interpolate(attrs.optHref);
scope.getHref = function(value, label) {
return hrefFormatter({ value: value, label: label });
};
}
Invoked like,
<directive-name
title ="name"
opt-href="/foo/bar/{{value}}" <
</directive-name>
Appreciate any pointers on what might have changed in angular to cause this or other pointers.
Instead of:
opt-href="/foo/bar/{{value}}"
Try this:
opt-href="'/foo/bar/{{value}}'"
Mind the additional '.
Thanks Michal! Used a controller function before the link to preserve the attrs.optHref in the scope. Something like,
controller: function ($scope, $element, $attrs) {
$scope.optHref = $attrs.optHref;
}
In the controller function the {{value}} was already interpolated in scope but existed in attrs. But in the link fn {{value}} was interpolated in attrs as well- so preserved it in scope and used scope.optHrefin the link fn.