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.
Related
I'm working on customize a input directive which including a label. I tried several days and refer to some articles.
The only problem is that except ng-change, ng-blur and ng-focus, all the other event work. https://jsfiddle.net/luneyq/mw3oz2pr/
Of course I can bind these three event manually myself and they can work as https://jsfiddle.net/luneyq/bp7f3z1o/
But I really don't know why ng-change, ng-blur and ng-focus don't work. Is there any special on these three event?
Anyone can help on this?
My codes are as below:
<div ng-app="myApp">
<div ng-controller="MainController">
<my-input type="number" name="valueNumber1" ng-model="obj.valueNumber1" label="Age" ng-click="log('click')" ng-change="log('change')" ng-blur="log('blur')" ng-focus="log('focus')" ng-mouseleave="log('mouseleave')"></my-input>
<div id="result"></div>
</div>
The JS:
var app = angular.module("myApp", []);
app.controller('MainController', function($scope, $window){
$scope.obj = {valueNumber1: 10};
$scope.log = function(text) {
document.getElementById("result").innerHTML = text + ':' + $scope.obj.valueNumber1 + "<br>" + document.getElementById("result").innerHTML;
};
});
app.directive('myInput', function() {
return {
require: '^ngModel',
restrict: 'EA',
scope: {
ngModel: '=',
name: '#name',
label: '#label'
},
replace: true,
transclude: true,
priority: 10,
template: '<div>' +
'<label for="{{ name }}">{{label}}</label>' +
'<input id="{{ name }}" ng-model="ngModel" />' +
'</div>',
compile: function(tElement, tAttrs, transclude) {
var tInput = tElement.find('input');
// Move the attributed given to 'custom-input' to the real input field
angular.forEach(tAttrs, function(value, key) {
if (key.charAt(0) == '$')
return;
tInput.attr(key, value);
});
tElement.replaceWith('<div class="cbay-input-div">' + tElement.html() + '</div>');
return;
}
};
});
Thanks in advance.
The issue is that compilation/transclusion/replace don't work the way you think they work (not sure where you made an incorrect assumption).
What is wrong with your code:
1). You are using incorrect attribute name: you should use tInput.attr(tAttrs.$attr[key], value); instead of tInput.attr(key, value);.
2). All the directives specified at my-input are compiled and linked despite your changes to the tElement in compile function. Proof is here - https://jsfiddle.net/fyuz3auc/3/, take a look at the myTest directive and its output in console: it is still applied to the myInput despite any of your effort.
3). You specified scope for your directive. Thus it has an isolated scope, thus anything you've compiled inside it has no access to log function of MainController, that's why your logs aren't working. Here is the fiddle proving that: https://jsfiddle.net/m5tba2mf/1/ (take a look at $scope.log = $scope.$parent.log in link function returned from compile).
In order to solve the second issue I suggest you to try alternative approach - I am using this at my project, and like it very much. The idea is to use terminal in conjunction with extremely high priority and $compile. Here is the updated fiddle, I think it is pretty straightforward what I do there: https://jsfiddle.net/uoat55sj/1/.
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.
(using angular and typescript)
I have a toggle switch directive as:
return {
restrict: 'E',
scope: {
ngModel: '=',
onText: '#',
offText: '#',
title: '#',
},
template:
'<label class="switch-light well">' +
' <input type="checkbox" ng-model="ngModel">' +
' <span>' +
' {{title}}' +
' <span class="switch-light-option" ng-class="{ \'switch-light-checked\': !ngModel}">{{offText}}</span>' +
' <span class="switch-light-option" ng-class="{ \'switch-light-checked\': ngModel}">{{onText}}</span>' +
' </span>' +
' <a class="btn btn-primary"></a>' +
'</label>'
};
In my html I am using this as:
toggle-switch ng-model="myValues" on-text="Enabled" off-text="Disabled"
Now I want to disable the above one. How can I do so.
I am pretty new to angular so any inputs would be appreciated.
Thanks
Use:
<toggle-switch
ng-model="switchStatus"
is-disabled="true">
</toggle-switch>
http://jumplink.github.io/angular-toggle-switch/
try adding the link code to your directive :
link: function ($scope, element, attrs) {
$scope.somethingDisabled = true;
};
and in your html part ng-disabled = somethingDisabled
then all you have to do is just add to the link function how and when you want to disabled either by passing it as an directive attr value or in the controller scope.
makes sense?
EDIT2
as per request I did something that would just show a proof of concept...
here is the plunker..
plunker
link: function ($scope) {
$scope.myclick = function(elem){
setTimeout(function(){$scope.disabled = true;},0);
setTimeout(function(){
$scope.disabled=false;
alert('db call done');
},2000);
};
}
I fixed the buggyness code might still be kinda ugly but Ill leave it to you to prettify it... the second timeout is the db call... first on is pushing the disable to the end of the stack so you'll see the animation.
if you want you can also just use !ngModel in the ng-disabled and then change the value of the ngModel when the db call come back..
good luck
I have the same issue and I used disabled attribute:
<toggle-switch label-text="'Enable Internet Cash'" label-position="'top-center'" disabled="MyBankAccount.HasMoney"></toggle-switch>
the disabled attribute receive a Boolean expression. If the disabled attribute has true value it will disable the toogle-switch , in other case it will enable the toggle-switch.
consider that in js file there is $scope.MyBankAccount and $scope.MyBankAccount is json object with HasMoney property.
I created following form as a angular directive and try to access the firstname from parent controller.
Please help me or guide me how to access directive form fields from inside MainCtrl.
HTML
<pre>
{{filter-frm| json}}
</pre>
<div ng-controller="MainCtrl">
<userform></userform>
</div>
JS
.controller('MainCtrl', ['$scope', function($scope){
console.log($scope.filterForm.firstname) //How to get this ?
};
.directive('userform', [function () {
return {
restrict: 'E',
scope: { formCtrl: '=' }
template: '<div>'
+ '<form id="filterForm" ng-submit="login()">'
+ '<input name="firstname" ng-model="user.firstName">'
+ '</form> '
+ '</div>',
link: function (scope, element, iAttrs) {
var form = element.find('form');
scope.formCtrl = form.controller('form');
}
};
}]);
If you add a name attribute to your form in your directive template, you will be able to access it via scope.formName. It should also bind the input fields values via scope.formName.fooInput.
In your case, adding name="filterForm" should do the trick:
template: '<div>'
+ '<form id="filterForm" name="filterForm" ng-submit="login()">'
+ '<input name="firstname" ng-model="user.firstName">'
+ '</form> '
+ '</div>',
Now you should be able to access scope.filterForm in the parent controller.
Param: name (optional); Type: string; Details: Name of the form. If specified, the form controller will be published into related scope, under this name.
from ngForm doc
You have to bind the values of the field to variables of your controller's model.
To do that, you have to expose the form fields as attributes of your directive.
See https://docs.angularjs.org/#!/guide/directive and how to use the scope field.