angular directive scope before return - angularjs

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);
});
}
};
});

Related

Angular: Change component template using variables

my question is: Is possible to change the component template when the value of a variable changes in the parent controller?
Here there are two ways that I tried to follow:
var topBarTemplateWithButton = [
'<section id="filters">',
'<div class="pull-left">',
'<h1>{{$ctrl.step}}</h1>',
'<div class="pull-left pageActionContainer">',
'<ng-transclude></ng-transclude>',
'</div>',
'</div>',
'</section>'].join(' ')
var topBarTemplateWithoutButton = [
'<section id="filters">',
'<div class="pull-left">',
'<h1>{{$ctrl.step}}</h1>',
'</div>',
'</section>'].join(' ')
myApp.component('topBar', {
bindings: {
step: '<'
},
template: this.templateToUse,
transclude: true,
controller: function() {
var me = this;
me.$onInit = function() {
this.templateToUse = topBarTemplateWithButton;
}
me.$onChanges = function(changes) {
if(changes.step.currentValue != "Projects") {
this.templateToUse = this.topBarTemplateWithoutButton;
}else {
this.templateToUse = topBarTemplateWithButton;
}
}
}
});
and
var topBarTemplateWithButton = [
'<section id="filters">',
'<div class="pull-left">',
'<h1>{{$ctrl.step}}</h1>',
'<div class="pull-left pageActionContainer">',
'<ng-transclude></ng-transclude>',
'</div>',
'</div>',
'</section>'].join(' ')
var topBarTemplateWithoutButton = [
'<section id="filters">',
'<div class="pull-left">',
'<h1>{{$ctrl.step}}</h1>',
'</div>',
'</section>'].join(' ')
myApp.component('topBar', {
bindings: {
step: '<'
},
template: '<div ng-include="$ctrl.templateToUse"/>,
transclude: true,
controller: function() {
var me = this;
me.$onInit = function() {
this.templateToUse = topBarTemplateWithButton;
}
me.$onChanges = function(changes) {
if(changes.step.currentValue != "Projects") {;
this.templateToUse = this.topBarTemplateWithoutButton;
}else {
this.templateToUse = topBarTemplateWithButton;
}
}
}
});
Both these two examples don't work. So it's possible to change the template of this component when the value of step changes? Thank you for your help.
the Template field can be a function that returns a template, and it takes attrs as an injectable. Here's an example that might accomplish what you're looking for.
template: function(attrs) {
"ngInject";
// check for custom attribute and return different template if it's there
},
Important point, however, is that these are uncompiled attributes at this point, because the template hasn't been compiled. It can't be compiled, in fact, until the template is determined. So them attribute you inspect can only be a string literal..
I think this is not the best practice to use two different templates for one component. The main rule is to use one controller with one view. So from this perspective it's better to achieve this behavior is to use ng-switch or ng-if inside component template or even to make two different dumb components - topBarWithButton and topBarWithoutButton. In these both cases you interact with component via bindings. I think this is a bit hacky to use attrs instead of using bindings. Components is not directves so you have to think in terms of components when you build component-based app. I think there are a lot of reasons why you shouldn't use attrs:
You can't change template after it was compiled.
You can't change state of your component.
You keep state in html template.
It's not easy to test this approach.

Select directive not working right in Firefox [duplicate]

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.

Angularjs, need best solution to move dynamic template formation and compilation code of multiple directives from controller to custom directive

I need to display multiple angularjs directives in a single page on tab click. It could be a combination c3 chart directives and ng grid directives. I am preparing the model with all these relevant parameters in the controller then forming the template and then compiling in the controller itself, which is perfectly working fine. As I realized doing DOM manipulation in controller is not a good practice, I am trying to do it in the custom directive.
This directive should support the following features :
The template should be combination of C3 chart directives.
The template can also have Angularjs ng Grid directive also along with c3 chart directives.
In future I also would like to use Good Map directive along with C3 chart and ng grid directives.
And some of these directives should be supported with custom dropdown.
For now I have used the following code in my controller which is perfectly working fine.
var template = '<div class= "chartsDiv">';
var dashletteId = 0;
var dashletterName = "chart";
var chartName = "";
for (var dashVar = 0; dashVar < data.tabDetails.length; dashVar++) {
dashletteId = data.tabDetails[dashVar].dashletteId; // Added
dashletterName = data.tabDetails[dashVar].dashletteName;
var axisType = data.tabDetails[dashVar].axisType;
var dataType = data.tabDetails[dashVar].dataType;
chartName = "chart" + eachTab.tabName.replace(/ +/g, "") + dashletteId ;
$scope[chartName] = {};
if (axisType == "timeseries") {
var xticksClassiffication = data.tabDetails[dashVar].xticksClassification;
var tickFormatFunction = {};
$scope[chartName] = {
data: {
x: 'x',
columns: data.tabDetails[dashVar].columns,
type: data.tabDetails[dashVar].dataType
},
axis: {
x: {
type: data.tabDetails[dashVar].axisType,
tick: {
format: data.tabDetails[dashVar].xtickformat
// '%Y-%m-%d'
}
}
},
subchart: {
show: true
}
};
}
if (dashletteId == 7) {
template += ' <div class="col"> <p class="graphtitle">' + dashletterName + ' </p> <span class="nullable"> <select ng-model="chartTypeSel" ng-options="eachChartType.name for eachChartType in chartTypeOptions" ng-change="transformChart(chartTypeSel, \'' + chartName + '\')"> </select> </span> <c3-simple id = "' + chartName + '" config="' + chartName + '"></c3-simple> </div>'
} else {
template += ' <div class="col"> <p class="graphtitle">' + dashletterName + ' </p> <c3-simple id = "' + chartName + '" config="' + chartName + '"></c3-simple> </div>';
}
}
template += ' </div>';
angular.element(document.querySelectorAll('.snap-content')).append($compile(template)($scope));
In order to make it simple I have provided only some sample code. Based on dashletteId, I have some specific requirements for which I am creating template dynamically based on dashletteId, all this code is perfectly working fine for me. Now my aim is to move all this template formation and compilation code from controller to a custom directive and I am looking for best possible solution for this, can any suggest me some pointers towards best solution.
For a specific user when he clicks any tab, what template has to be formed for compilation is predefined. So I can get that either during ng-init function call or tab's click (i.e, select) function call.
The following is sample code for my ng grid template formation.
if (axisType == "table") {
var config = {
9: {
gridOptions: 'gridOptionsOne',
data: 'dataOne',
columnDefs: 'colDefsOne'
},
10: {
gridOptions: 'gridOptionsTwo',
data: 'dataTwo',
columnDefs: 'colDefsTwo'
},
11: {
gridOptions: 'gridOptionsThree',
data: 'dataThree',
columnDefs: 'colDefsThree'
},
18: {
gridOptions: 'gridOptionsFour',
data: 'dataFour',
columnDefs: 'colDefsFour'
}
};
$scope.getColumnDefs = function(columns) {
var columnDefs = [];
columnDefs.push({
field: 'mode',
displayName: 'Mode',
enableCellEdit: true,
width: '10%'
});
columnDefs.push({
field: 'service',
displayName: 'Service',
enableCellEdit: true,
width: '10%'
});
angular.forEach(columns, function(value, key) {
columnDefs.push({
field: key,
displayName: value,
enableCellEdit: true,
width: '10%'
})
});
return columnDefs;
};
if (dataType == "nggridcomplex") {
$scope.serverResponse = {
columns: data.tabDetails[dashVar].columns,
data: data.tabDetails[dashVar].data
};
$scope[config[dashletteId].columnDefs] = $scope.serverResponse.columns;
$scope[config[dashletteId].data] = $scope.serverResponse.data;
} else {
if (dashletteId == 18) {
$scope.serverResponse = {
columns: data.tabDetails[dashVar].timespans[0], // This is for column headers.
data: data.tabDetails[dashVar].columns
};
} else {
$scope.serverResponse = {
columns: data.tabDetails[dashVar].timespans[0], // This is for column headers.
data: data.tabDetails[dashVar].columns
};
}
$scope[config[dashletteId].columnDefs] = $scope.getColumnDefs($scope.serverResponse.columns);
$scope[config[dashletteId].data] = $scope.serverResponse.data;
}
$scope[config[dashletteId].gridOptions] = {
data: config[dashletteId].data,
showGroupPanel: true,
jqueryUIDraggable: false,
columnDefs: config[dashletteId].columnDefs
};
template += ' <div class="col"> <p class="graphtitle">' + dashletterName + ' </p> <div class="gridStyle" ng-grid="' + config[dashletteId].gridOptions + '"></div>';
}
So in a single page I need to show four directives, it could be 3 c3 charts and 1 ng Grid table directive, or 2 C3 charts and 2 ng Grids tables, etc based on the predefined choice made by the user.
The following is preliminary code of my custom directive before working on further on this I thought of taking input from others for better approach. Here in my link function the template I need to get dynamically from controller upon tab click or ng-init phase, etc.
app.directive('customCharts', ['$compile', function($compile) {
return {
restrict: 'EA',
scope: {
chartName: '='
},
link: function(scope, element) {
var template = ' <div class="col"> <p class="graphtitle">' + dashletterName + ' </p> <c3-simple id = "' + chartName + '" config="' + chartName + '"></c3-simple> </div>'
var parent = angular.element(document.querySelectorAll('.chartsDiv')) // DOM element where the compiled template can be appended
var linkFn = $compile(template);
var content = linkFn(scope);
parent.append(content);
}
}
}]);
Please let me know if I need to provide any further clarification to my question. Any directions please with some sample code.
I would recommend creating one or more directives that each add a single component to the DOM - so 1,2, and 3 should be separate directives. In my experience, if you create a directive that adds multiple components you may end up trading an unwieldy controller for an unwieldy directive.
As for things like config and columnDefs, I would define configuration in the controller, or in a separate service/factory that is passed to the controller if you want to keep your controller really light, and then pass the config to the directive scope. This will allow you to re-use a directive within or across apps.
To watch for controller changes, in the directive scope.$watch() the appropriate attributes and then call the appropriate update functions in the directive. I believe this is what you mean by 4. This is demonstrated by the dropdown in my example.
I've had success without using $compile, but maybe others can argue for it.
Word of caution: If you are using a third party library that uses asynchronous functionality that Angular isn't aware of, you may need to put your directive's update behavior inside of a scope.$apply(), or else the DOM won't update until the next Angular digest cycle.
If your HTML is more than a line or two, I would recommend using templateUrl.
Here is a toy example using jQuery in a trivial way.
app.directive('chart', function() {
return {
restrict: 'EA',
scope: {config: '='},
templateUrl: 'chart.html',
link: function(scope, element) {
var el = element[0];
function update() {
$(el).html('<h4>' + scope.config.chartType + '</h4>');
}
scope.$watch('config', update, true);
}
}
});

scope.$eval doesn't work in angular.js directive

I want to build a angular.js directive, which by clicking the <span>, it will turn out into a editable input. and the following code works well, except when the model is empty or model length has 0, make it show <span> EMPTY </span>.
<span ng-editable="updateAccountProfile({'location':profile.location})" ng-editable-model="profile.location"></span>
app.directive('ngEditable', function() {
return {
template: '<span class="editable-wrapper">' + '<span data-ng-hide="edit" data-ng-click="edit=true;value=model;">{{model}}</span>' + '<input type="text" data-ng-model="value" data-ng-blur="edit = false; model = value" data-ng-show="edit" data-ng-enter="model=value;edit=false;"/>' + '</span>',
scope: {
model: '=ngEditableModel',
update: '&ngEditable'
},
replace: true,
link: function(scope, element, attrs) {
var value = scope.$eval(attrs.ngEditableModel);
console.log('value ' , attrs.ngEditableModel , value);
if (value == undefined || (value != undefined && value.length == 0)) {
console.log('none');
var placeHolder = $("<span>");
placeHolder.html("None");
placeHolder.addClass("label");
$(element).attr("title", "Empty value. Click to edit.");
}
scope.focus = function() {
element.find("input").focus();
};
scope.$watch('edit', function(isEditable) {
if (isEditable === false) {
scope.update();
} else {
// scope.focus();
}
});
}
};
});
the problem occurs at the this part of code that
var value = scope.$eval(attrs.ngEditableModel);
console.log('value ' , attrs.ngEditableModel , value);
attrs.ngEditableModel output the content 'profile.location', but then using scope.$eval() only output ' undefined ', even model 'profile.location' is not null
You have two ways to fix that.
1) You're calling $eval on the wrong scope. You have in scope in your link function your newly created isolated scope. attrs.ngEditableModel does contain a reference to the outer scope of your directive, which means you have to call $eval at scope.$parent.
scope.$parent.$eval(attrs.ngEditableModel)
or 2) a better way to handle it: You already have bound ngEditableModel via your scope definition of
scope: {
model: '=ngEditableModel',
So, instead of using your own $eval call, you can just use scope.model which points to the value of attrs.ngEditableModel. This is already two-way-bound.

Directive with <select ng-options> and indirection

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.

Resources