(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.
Related
I need to access a scope variable before entering the return of a directive.
I have a directive which returns a select element with options for each of the trucks.
<tc-vehicle-select label="Truck" selected="activeDailyLog.truck"></tc-vehicle-select>
I need to use the selected value in the directive to put a selected tag on the appropriate option element.
.directive('tcVehicleSelect', function(localStorageService) {
/* Get a list of trucks for the organization and populate a select element for
the user to choose the appropriate truck.
*/
var trucks = localStorageService.get('trucks');
var truckHtml;
for (var i = 0; i < trucks.length; i++) {
var truck = trucks[i];
var injectText;
if(truck.description){
injectText = truck.description
}else{
injectText = 'truck ' + truck.id
}
truckHtml += '<option value="' + truck.id + '">' + injectText + '</option>'
}
return {
scope: {
label: '#',
active: '#'
},
replace: true,
template: '<label class="item item-input item-select">' +
'<div class="input-label">{{label}}</div>' +
'<select ng-model="timeLog.truck"><option value="">None</option>' + truckHtml +
'</select></label>'
};
});
I have everything working in this directive except I'm stuck on setting the selected attribute on the correct element. If I could access the selected variable passed in I would be able to do it by inserting into the truckHtml, but I haven't found examples using that - only using the variables below in the retrun block.
Any ideas?
UPDATE: Also wanted to clarify that the activeDailyLog.truck in the HTML has the correct value I'm looking for.
It makes sense to place your directive's code inside the link function.
To retrieve the passed scope variable inside the directive, use = for two-way binding to the same object.
Code:
.directive('tcVehicleSelect', function(localStorageService) {
/* Get a list of trucks for the organization and populate a select element for
the user to choose the appropriate truck.
*/
return {
scope: {
selected: '='
},
replace: true,
template: '<label class="item item-input item-select">' +
'<div class="input-label">{{label}}</div>' +
'<select ng-model="timeLog.truck"><option value="">None</option>' + truckHtml +
'</select></label>',
link: function(scope, elem, attrs) {
var trucks = localStorageService.get('trucks');
trucks.forEach(function(truck) {
var injectText;
if(truck.description){
injectText = truck.description
} else {
injectText = 'truck ' + truck.id
}
truckHtml += '<option value="' + truck.id + '">' + injectText + '</option>'
}
// Access scope.selected here //
console.log(scope.selected);
}
};
});
Also replaced with Array.forEach() method, as it seemed more relevant in this context!
Internally there are very few differences between directive and a factory. The most important thing here is that directives are cached - your code is only run once and angular will keep reusing its return value every time it needs to use that directive.
That being said, you cannot access scope within directive declaration body - if you could it would be a scope of the first directive and its result would be cached and used for all other places you would use same directive.
I do like the idea of building the options before the return though, assuming you are sure it will not change during the application life (as it will save you from some unnecessary bindings). However, it is usually not true, so I would rather move the whole logic into a compile or even link function:
.directive('tcVehicleSelect', function(localStorageService) {
return {
scope: {
label: '#',
active: '#'
},
replace: true,
template: '<label class="item item-input item-select">' +
'<div class="input-label">{{label}}</div>' +
'<select ng-model="timeLog.truck"><option value="">None</option>'
'</select></label>',
link: function(scope, element) {
var trucks = localStorageService.get('trucks'),
select = element.find('select');
option;
trucks.forEach(function(truck) {
option = angular.element('<option></option>');
option.attr('value', truck.id);
option.html(truck.description || "truck" + truck.id);
if (truck.id === scope.selected.id) {
option.attribute('selected', 'selected');
}
select.append(option);
});
}
};
});
I have several multi-selects on a page, each with a bit of logic that fills that multi-select from the server, and I want to wrap each one up into a Directive.
Before trying to wrap these into Directives, I built each as such:
index.html
<select name="groups" ng-model="inputs.groups" ng-change="groupsChanged()" ng-options="g for g in allgroups" multiple></select>
controllers.js
In the first pass, I do my $http calls from here. Yes, I know, not best practices, but I wanted to prove that this works to myself first.
$scope.loadSelect = function(_url) {
$http({
url: _url,
method: 'POST',
data: $scope.inputs,
model: 'all' + _url
}).success(function(data, status, headers, config) {
$scope[config.model] = data;
});
};
// Fill groups
$scope.loadSelect('groups');
// When groups change, reload other select fields that depend on groups
$scope.groupsChanged = function() {
$scope.loadSelect('categories');
$scope.loadSelect('actions');
}
Now I want to migrate this to a Directive. I see two major challenges:
1.) How do I encapsulate the entire set of options (e.g. what is now the "allgroups" model) into the Directive?
2.) Based on initial experiments, I tried to physically build the <select/> into the template, but realized that I have to manipulate the DOM to physically replace name, ng-model, and ng-options. That lead me to the compile attribute, but a.) that feels wrong and b.) setting <select ng-options="x for x in allgroups" /> doesn't actually repeat after it's been inserted into the DOM. Using compile doesn't feel right; what's the right way to approach this?
Here is my first attempt at the Directive looks like this. It doesn't really work, and I think I'm going about it incorrectly:
index.html
<dimension ng-model="inputs.users" alloptions-model="allusers">Users</dimension>
directives.js
directive('dimension', function() {
return {
restrict: 'E',
scope: {
ngModel: '=',
alloptionsModel: '='
},
template:
'<div>' +
'<label ng-transclude></label>' +
'<fieldset>' +
'<div class="form-group">' +
'<select ng-model="{{ngModel}}" ng-options="x for x in {{alloptionsModel}}" multiple class="form-control"></select>' +
'</div>' +
'</fieldset>' +
'</div>',
replace: true,
transclude: true
};
});
Clearly I haven't even gotten to the server load part yet, but I plan to roll that into a controller in the Directive, with the actual $http call in a service.
I feel like I'm moving down the wrong track. If you have suggestions on how to realign, please help!
The main problem with your directive is that you can't use mustache binding in ngModel and ngOptions directive because they are evaluated directly. You can directly bind to the scoped property (ngModel and alloptionsModel):
directive('dimension', function() {
return {
restrict: 'E',
scope: {
ngModel: '=',
alloptionsModel: '='
},
template:
'<div>' +
'<label ng-transclude></label>' +
'<fieldset>' +
'<div class="form-group">' +
'<select ng-model="ngModel" ng-options="x for x in alloptionsModel" multiple class="form-control"></select>' +
'</div>' +
'</fieldset>' +
'</div>',
replace: true,
transclude: true
};
});
See this plunkr for a working example.
Edit
As for the compile route, there is nothing wrong with it. It is useful when you need to dynamically create a template which will clearly be your case when you will get to the select's item template.
compile: function(tElement, tAttrs) {
var select = tElement.find('select'),
value = tAttrs.value ? 'x.' + tAttrs.value : 'x',
label = tAttrs.label ? 'x.' + tAttrs.label : 'x',
ngOptions = value + ' as ' + label + ' for x in alloptionsModel';
select.attr('ng-options', ngOptions);
}
// In the HTML file
<dimension ng-model="inputs.users"
alloptions-model="allusers"
label="name">
Users
</dimension>
I've updated the plunkr with the compile function.
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
I have several multi-selects on a page, each with a bit of logic that fills that multi-select from the server, and I want to wrap each one up into a Directive.
Before trying to wrap these into Directives, I built each as such:
index.html
<select name="groups" ng-model="inputs.groups" ng-change="groupsChanged()" ng-options="g for g in allgroups" multiple></select>
controllers.js
In the first pass, I do my $http calls from here. Yes, I know, not best practices, but I wanted to prove that this works to myself first.
$scope.loadSelect = function(_url) {
$http({
url: _url,
method: 'POST',
data: $scope.inputs,
model: 'all' + _url
}).success(function(data, status, headers, config) {
$scope[config.model] = data;
});
};
// Fill groups
$scope.loadSelect('groups');
// When groups change, reload other select fields that depend on groups
$scope.groupsChanged = function() {
$scope.loadSelect('categories');
$scope.loadSelect('actions');
}
Now I want to migrate this to a Directive. I see two major challenges:
1.) How do I encapsulate the entire set of options (e.g. what is now the "allgroups" model) into the Directive?
2.) Based on initial experiments, I tried to physically build the <select/> into the template, but realized that I have to manipulate the DOM to physically replace name, ng-model, and ng-options. That lead me to the compile attribute, but a.) that feels wrong and b.) setting <select ng-options="x for x in allgroups" /> doesn't actually repeat after it's been inserted into the DOM. Using compile doesn't feel right; what's the right way to approach this?
Here is my first attempt at the Directive looks like this. It doesn't really work, and I think I'm going about it incorrectly:
index.html
<dimension ng-model="inputs.users" alloptions-model="allusers">Users</dimension>
directives.js
directive('dimension', function() {
return {
restrict: 'E',
scope: {
ngModel: '=',
alloptionsModel: '='
},
template:
'<div>' +
'<label ng-transclude></label>' +
'<fieldset>' +
'<div class="form-group">' +
'<select ng-model="{{ngModel}}" ng-options="x for x in {{alloptionsModel}}" multiple class="form-control"></select>' +
'</div>' +
'</fieldset>' +
'</div>',
replace: true,
transclude: true
};
});
Clearly I haven't even gotten to the server load part yet, but I plan to roll that into a controller in the Directive, with the actual $http call in a service.
I feel like I'm moving down the wrong track. If you have suggestions on how to realign, please help!
The main problem with your directive is that you can't use mustache binding in ngModel and ngOptions directive because they are evaluated directly. You can directly bind to the scoped property (ngModel and alloptionsModel):
directive('dimension', function() {
return {
restrict: 'E',
scope: {
ngModel: '=',
alloptionsModel: '='
},
template:
'<div>' +
'<label ng-transclude></label>' +
'<fieldset>' +
'<div class="form-group">' +
'<select ng-model="ngModel" ng-options="x for x in alloptionsModel" multiple class="form-control"></select>' +
'</div>' +
'</fieldset>' +
'</div>',
replace: true,
transclude: true
};
});
See this plunkr for a working example.
Edit
As for the compile route, there is nothing wrong with it. It is useful when you need to dynamically create a template which will clearly be your case when you will get to the select's item template.
compile: function(tElement, tAttrs) {
var select = tElement.find('select'),
value = tAttrs.value ? 'x.' + tAttrs.value : 'x',
label = tAttrs.label ? 'x.' + tAttrs.label : 'x',
ngOptions = value + ' as ' + label + ' for x in alloptionsModel';
select.attr('ng-options', ngOptions);
}
// In the HTML file
<dimension ng-model="inputs.users"
alloptions-model="allusers"
label="name">
Users
</dimension>
I've updated the plunkr with the compile function.
I have this fiddle
angular.module('mainApp', [])
.directive('list', function factory() {
return {
restrict: 'E',
scope: {
size: '=',
listWidth: '#'
},
replace: true,
template: '<ul class="syn-list"><li ng-repeat="item in items">{{item}}</li></ul>',
link: function(scope, element, attrs) {
scope.items = ['item 1', 'item 2', 'item 3'];
$('#p1').html(scope.size + ' - ' + attrs.size + ' :: ' + scope.listWidth + ' - ' + attrs.listWidth);
setTimeout(function() {
$('#p2').html(scope.size + ' - ' + attrs.size + ' :: ' + scope.listWidth + ' - ' + attrs.listWidth);
}, 0);
}
};
});
Why scope.size is set to the expected value and scope.listWidth returns undefined?
And why inside a setTimeout call with 0 seconds scope.listWidth is set to the expected value?
When using # this happens
because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.When you add settimeout(0) gives a pause which gives browser a chance to do the interpolation. When using = there is no interpolation you directly use $parse service avaialable in angular to use the parent scope properties so there are not undefined without setTimeout.You should not be directly using scope.value instead put a $observe property on the required attribute