Invalidate select when the first option is selected - javascript

I am going to validate select box (ngOption) by first value (if first value is selected - false, if not - true) and I do not want to use predefined value within select box like this:
<select id="mainVideo" class="form-control"
ng-model="qtTestData.mainVideo"
ng-options="mainVideo for mainVideo in mainVideoSources"
name="mainVideo"
required>
<option value="">-- Please, select a value --</option>
</select>
So, I have such select box:
<select id="mainVideo" class="form-control" requiredSelect
ng-model="qtTestData.mainVideo"
ng-options="mainVideo for mainVideo in mainVideoSources"
name="mainVideo"
required>
</select>
My array is:
$scope.mainVideoSources = ['Please, select a value', 'Value1', 'Value2'];
I am using this directive to define if first value selected (it means that user did not change the value)
App.directive('requiredSelect', function () {
return {
restrict: 'AE',
require: 'ngModel',
link: function(scope, element, attributes, ngModel) {
if (!ngModel) return;
attributes.requiredSelect = true;
var validator = function(value) {
if ( value != 'Please, select a value' ) {
ngModel.$setValidity('requiredSelect', false);
return false;
} else {
ngModel.$setValidity('requiredSelect', true);
return true;
}
};
ngModel.$formatters.push(validator);
ngModel.$parsers.unshift(validator);
attributes.$observe('requiredSelect', function() {
validator(ngModel.$viewValue);
});
}
};
});
HTML that should appear if invalid value selected:
<div class="has-error bg-danger" ng-show="qtTestForm.mainVideo.$error.requiredSelect">
<p class="text-center">Oops, something wasn't right</p>
</div>
But it doesn't work...
And How can statement (value != 'Please, select a value') be rewritten in Angular way? In JS
it's something like this select.selectedIndex == 0

You could just separate your data into "data values" and "display values":
sources = [{
value: '',
label: 'Please select a value'
}, {
value: 'value1'
}, ...]
and then use
<select ng-options="item.value as item.label for item in sources" required ...></select>
to make the required validator do the rest. No need to create your own directive.
As a sidenote, if you are creating a validation directive, use ngModelController.$validators (not $parsers & $formatters). Also, you don't need to check for ngModel presence, if you have it as mandatory requirement.

Instead of array of values, use array of objects, like this:
$scope.mainVideoSources = [ { "value": 0, "text": "Please, select a value" }, {{ "value": 0, "text": "Value1"}, { "value": 0, "text": "Value2"} ];

#hon2a,
Native Angular validation works good, thanks. I have rewritten my directive on $validators. Now it looks
App.directive('requiredSelect', function () {
return {
restrict: 'AE',
require: 'ngModel',
link: function(scope, element, attributes, ngModel) {
ngModel.$validators.requiredSelect = function(modelValue, viewValue) {
var value = modelValue || viewValue;
var requiredSelect = 'Please, select a value'; //TBD.. select first ngOption
return value != requiredSelect;
};
}
};
});
and it works for me.

Related

AngularJS: ngChange not called from directive

Brief intro to my problem
I have a directive that dynamically shows a list of checkboxes. It has a parameter called options that should be an array like the following, in order to show the list of checkboxes correctly. For example:
var options = [
{
id: 1,
label: 'option #1'
},
{
id: 2,
label: 'option #2'
},
{
id: 3,
label: 'option #3'
}
];
So, by passing this array to my directive, a group of three checkboxes would be shown.
Also, the directive requires ngModel that will have the result of checking/unchecking the checkboxes (this object is always passed initialized). For example:
var result = {
"1": true,
"2": true,
"3": false
};
This case means that the first and second checkboxes (options with id=1 and id=2) are checked and the third (option with id=3) is unchecked.
My directive
template.html
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]">
{{ ::option.label }}
</label>
</div>
</div>
directive.js
angular
.module('myApp')
.directive('myDirective', myDirective);
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
};
};
What I want to achieve
Wherever I use my directive, I want to be able to trigger a function whenever the ngModel changes. Of course, I would like to achieve this using ngChange. So far I have the following:
<my-directive
name="myName"
options="ctrlVM.options"
ng-model="ctrlVM.result"
ng-change="ctrlVM.selectionChanged()">
</my-directive>
but the .selectionChanged() function is not triggered whenever the model changes. Anyone has any idea why this is not working as I am expecting it to work?
First thing first, please try to provide jsfiddle, codepen etc code snippet link so that it will be easy for others to answer your question.
The problem in your case is that you are never updating the ctrlVM.result object as you are passing the object's reference and that reference never change even if you manually update the model by calling ngModel.$setViewValue().
To solve the problem, just update the model by manually calling ngModel.$setViewValue() and pass in the new Object so that the reference changes and that will trigger the ngChange directives logic.
I've added the logic to do that and it will successfully trigger the change. Look at the code below:
angular
.module('myApp', [])
.directive('myDirective', myDirective)
.controller('MyController', function($timeout) {
var vm = this;
vm.options = [{
id: 1,
label: 'option #1'
}, {
id: 2,
label: 'option #2'
}, {
id: 3,
label: 'option #3'
}];
vm.result = {
"1": true,
"2": true,
"3": false
};
vm.selectionChanged = function() {
vm.isChanged = true;
$timeout(function() {
vm.isChanged = false;
}, 500)
}
});
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
scope.updateValue = function(val) {
ngModel.$setViewValue(Object.assign({}, val))
}
};
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js"></script>
<div ng-app="myApp">
<script type="text/ng-template" id="template.html">
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]" ng-click="updateValue(result)">
{{ ::option.label }}
</label>
</div>
</div>
</script>
<div ng-controller="MyController as ctrlVM">
<my-directive name="myName" options="ctrlVM.options" ng-model="ctrlVM.result" ng-change="ctrlVM.selectionChanged()">
</my-directive>
<div> Data: {{ctrlVM.result}} </div>
<div> isChanged: {{ctrlVM.isChanged}} </div>
</div>
</div>
#Gaurav correctly identified the problem (ng-change is never called because the object reference does not change). Here is a simpler solution that doesn't require manually cloning into the controller's model:
Add a binding for the ng-change attribute:
scope: {
options: '=',
ngChange: '&' // Add this, creates binding to `ctrlVM.selectionChanged()`
}
Add an ng-change to your checkbox template:
<input type="checkbox"
ng-model="result[option.id]" ng-change="ngChange()">
Now, when any checkbox changes it will automatically call the outer ng-change function without the intermediate step of cloning into the model.

Why is my angular directive (using compile) blocking input in some scenarios?

I've written a directive which adds a class based on a condition - see snippet at the bottom of the question.
It works as expected in the following simple usage scenario for a required field:
<input type="text" name="lastName" ng-model="$crtl.lastName" my-directive="$crtl.isLastNameValid()" required>
However in the following scenario where I have two dependent elements using ng-required it blocks input on the element in which I don't type initially.
i.e. if I type in email it blocks input into mobile and visa versa - other than that is works fine, used as:
<input type="email" id="email" name="email" ng-model="$ctrl.emailAddress"
ng-required="$ctrl.mobileNumber.length === 0" my-directive="$ctrl.isEmailValid()">
<input type="tel" id="mobile" name="mobile" ng-model="$ctrl.mobileNumber"
pattern="(?:\+?61|0)4 ?(?:(?:[01] ?[0-9]|2 ?[0-57-9]|3 ?[1-9]|4 ?[7-9]|5 ?[018]) ?[0-9]|3 ?0 ?[0-5])(?: ?[0-9]){5}"
ng-required="$ctrl.emailAddress.length === 0" my-directive="$ctrl.isMobileValid()">
Where am I going wrong? I am compiling the element based on the condition passed in I am assuming it has something to do with that?
export const myDirective = ($compile: ng.ICompileService): ng.IDirective => {
return {
restrict: 'A',
scope: true,
compile: (element: ng.IAugmentedJQuery, attrs: ng.IAttributes): ng.IDirectivePrePost => {
var condition = attrs['myDirective'];
element.removeAttr('my-directive');
if (condition) {
element.attr('ng-class', `{ "validation-error": ${condition} }`);
return {
pre: () => { },
post: ($scope: ng.IScope, element: ng.IAugmentedJQuery) => {
$compile(element)($scope);
}
};
}
return {
pre: () => { },
post: () => { }
};
}
};
};
If you want a directive that adds and removes a class based on a condition defined by an angular expression:
app.directive("myDirective", function () {
return function postLink (scope, elem, attrs) {
scope.$watch(attrs.myDirective, function(newBool) {
if (newBool) {
attrs.$addClass("validation-error");
} else {
attrs.$removeClass("validation-error");
};
});
};
});
On every digest cycle, the directive evaluates the Angular Expression defined by the my-directive attribute and if the expression changes, it either adds or removes the validation-error class based in the truthyness of the Angular Expression.

AngularJS: Why is my directive's link function not being called?

I am using ngTagsInput and would like to perform some validation on my models as they change. Here is a Plunker of what I'd like to achieve.
Markup:
<div ng-repeat="field in fields">
<tags-input ng-model="field.selectedData" max-tags="{{field.maxTags}}" enforce-max-tags placeholder="{{option.placeholder}}">
<auto-complete source="updateAutocomplete($query)"></auto-complete>
</tags-input>
</div>
Fields / models:
$scope.fields = [
{
name: 'assay',
placeholder: 'Select one assay...',
maxTags: 1,
selectedData: [] // These are the models
},
{
name: 'cellLines',
placeholder: 'Select cell line(s)...',
maxTags: 5,
selectedData: []
},
...
]
Finally, my enforceMaxTags directive:
.directive('enforceMaxTags', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function(value) {
console.log('link called');
});
}
};
})
The enforceMaxTags directive is being compiled, but the link function is not called even when the models change. The data binding does appear to work, though, because if I console.log the selectedData on form submission, it is filled with the correct objects. What am I missing?
Thanks in advance.

AngularJS - Fill input text field with dropdown menu

How can I fill an input text field with a dropdown-menu ?
Text Input:
<input type="text" ng-model="storagePlaceModel" lms-dropdown class="form-control lms-input-text-disable lms-dropdown-toggle" id="storagePlace" placeholder="Standort" autocomplete="off" readonly>
Own written dropdown:
<div class="lms-dropdown">
<div class="lms-dropdown-scrollbar">
<li ng-repeat="data in data_input_fields.input_cat_music_book_storage_place">
<span>{{data.input_value}}</span>
<i ng-show="storagePlaceModel == data.input_value" class="glyphicon glyphicon-ok"></i>
</li>
</div>
</div>
If I select an li element I want to update the storagePlaceModel with the li value.
Updated question:
Because I have more than one of these text inputs with dropdowns I need a solution where the conroller/directive does not know the model name exactly.
Directive could look like:
lmsApp.directive('updateInputField', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
$(elem).click(function(e) {
// Read out the model name of the html text input field
});
}
};
});
Thank you for your help! I would appreciate every answer.
I've edited the entire question to create a directive to wrap your desired structure. You'll pass to the directive the model you want, and that way, each model will be independent on different directive usages:
myApp.directive('myDirective', function() {
return {
restrict: "E",
scope: {
model: "=",
datas: "="
},
templateUrl: "directive.html",
link: function(scope, element, attr) {
scope.updateValue = function(val) {
scope.model.storagePlaceModel = val;
}
}
}
});
The directive.html contains your text input and the dropdown.
Controller:
function MyCtrl($scope) {
$scope.wrapper = {};
$scope.wrapper2 = {};
$scope.wrapper3 = {};
$scope.datas = [
{ "input_value" : "1" },
{ "input_value" : "2" },
{ "input_value" : "3" },
{ "input_value" : "4" }
];
}
HTML usage:
<div ng-app="myApp" ng-controller="MyCtrl">
<my-directive model="wrapper" datas="datas"></my-directive>
<my-directive model="wrapper2" datas="datas"></my-directive>
<my-directive model="wrapper3" datas="datas"></my-directive>
</div>
Working Fiddle

angular: how to handle code provided by attributes

As an example of what I want, consider the following example
<select ng-options="option.text for option in options"></select>
In my directive I want to use something similar to ngOptions, because I need to create a list
For example, assume I have a directive barFoo, called as follows:
<bar-foo options="options"></bar-foo>
with a template/html as follows:
<ol>
<li ng-repeat="option in options" ng-bind="option.text"></li>
</ol>
What is needed to change all this into a call like
<bar-foo options="option.text for option in options"></bar-foo>
The main reason I need this is because I don't know the property name holding the label text (in this case it is text)
I provided a fiddle and see whether this helps. Instead of passing in "options.text for option in options", I set it up such that you pass the "options" array and then the field you want. I assumed the field will be set up as a variable; if it hard-coded, then you can just do field='someFieldName' instead.
http://jsfiddle.net/y376K/1/
HTML
<body ng-app='testApp'>
<div ng-controller='TestCtrl'>
<bar-foo options='options' field='{{optionsField}}'></bar-foo>
</div>
</body>
JS
angular.module('testApp', [])
.controller('TestCtrl', function($scope) {
$scope.options = [
{
text: 'Node.js rocks my socks',
language: 'Node.js',
},
{
text: 'Angular is hot',
language: 'Angular.js',
},
{
text: 'Backbone.js is mmmm',
language: 'Backbone.js',
}
];
$scope.optionsField = 'text';
})
.directive('barFoo', function() {
return {
restrict: 'E',
scope: {
options: '=',
field: '#'
},
template: '<ol><li ng-repeat="option in options" ng-bind="option[field]"></li>'
};
})
You can do this by parsing the attribute. The other solution would be to pass it as two attributes (see the other answer)
You should probably use a regexp for this, but I coded this quickly:
app.directive('barFoo',function($parse) {
return {
restrict: 'E',
scope: {},
templateUrl: "template.html",
link: function(scope,element,attrs) {
var splitOptions = attrs.options.split(' for ');
scope.fieldName = splitOptions[0].split('.')[1];
var repeatExp = splitOptions[1];
scope.valueName = repeatExp.split(' in ')[0];
var collectionName = repeatExp.split(' in ')[1];
scope.values = $parse(collectionName)(scope.$parent);
}
};
});
See this plnkr

Categories