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

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.

Related

text input to variable in AngularJS

I want to get the data written in two text inputs saved into variables.
This is my rtmNav.html:
<div class="rtm-nav">
<label>From:
<input type="text" name="input" ng-model="ctrl.dataa.from">
</label>
<label>To:
<input type="text" name="input" ng-model="ctrl.dataa.to">
</label>
</div>
This is the controller that already exists. I cannot change it's structure and imo it looks different compared to a classic one. I must add the variables here:
demand.js
class DemandCtrl {
constructor(ChartDataService) {
this.ChartDataService = ChartDataService;
}
$onInit() {
getData.call(null, this);
}
/////////////// THIS IS WHERE I GUESS I SHOULD ADD THE VARIABLES
this.dataa = {
from: '',
to: ''
};
////////////
}
... other methods ...
export const Demand = {
bindings: {
data: '<'
},
templateUrl: demandPageHtml,
controller: DemandCtrl
};
Probably it is not the correct way to add it here like that because I get this message from my code editor:
[js] Unexpected token. A constructor, method, accessor, or property
was expected.
Any ideas how to solve this?
Put your variable declaration into the constructor
class DemandCtrl {
constructor(ChartDataService) {
this.ChartDataService = ChartDataService;
this.dataa = {
from: '',
to: ''
};
}
$onInit() {
getData.call(null, this);
}
}

Angular typeahead asynchronous results - receiving error "TypeError: Cannot use 'in' operator to search for ... in ..."

I have made a directive for an Angular UI typeahead field. I am trying to design it so that as a user is typing, it sends asynchronous backend calls for results that will populate the dropdown that appears, as demonstrated in the Angular-bootstrap docs, example 2. However, when I start typing ('a' in this case), I get the error:
TypeError: Cannot use 'in' operator to search for 'getters' in a
Here is the factory method that makes the REST call:
certFactory.getUsersWithMatchingUsername = function(username) {
return $http.get(urlBase + '/managed/user?_queryFilter=userName+co+' + '\'' + username + '\'', {timeout: timeoutLength})
.then(function(response) {
// success
return response.data;
}, function(response) {
// error
return $q.reject(response);
});
};
Here is the controller method that calls the factory method:
$scope.getters = {
getUsersWithUsername: function (username) {
certFactory.getUsersWithMatchingUsername(username)
.then(function (response) {
var users = [];
angular.forEach(response.result, function(item) {
users.push(item);
})
return users;
}, function (error) {
console.log('failed!')
})
}
Here is my directive:
angular.module('myApp')
.directive('dropdownsearch', function(){
return {
restrict: 'E',
transclude: true,
scope: {
getterFn: '&',
config: '=', // object with properties label and type
disabled: '=?ngDisabled',
required: '=?ngRequred',
ngModel: '=',
options: '='
},
require: ['^form', 'ngModel'],
templateUrl: 'views/templates/dropdownSearchView.html',
replace: true,
link: function(scope, iElm, iAttrs, controller) {
if (iAttrs.required !== undefined) {
// If attribute required exists
// ng-required takes a boolean
scope.required = true;
}
if (iAttrs.readonly !== undefined) {
// If attribute required exists
// ng-required takes a boolean
scope.readonly = true;
}
}
}
}
);
Here is the directive template:
<div class="form-group has-feedback">
<label class="control-label"> Choose {{ config.type }}></label>
<div class="dropdown dropdown">
<div class="input-group">
<input
type="text"
class="form-control"
placeholder="Make selection"
ng-model="ngModel"
uib-typeahead="option as option[config.label] for option in getterFn($viewValue)"
typeahead-editable="false"
ng-required="required"
ng-disabled="disabled"
/>
</div>
</div>
</div>
And finally, here is my directive in use:
<dropdownsearch ng-show= 'fieldMethods.showSubfield(subfield)'
getter-fn="getters.getUsersWithUsername"
ng-model="subsubfield.value"
config="fieldMethods.getConfig(subfield)">
</dropdownsearch>
Any help would be greatly appreciated. Also, let me know if any additional info is required.
The UI Bootstrap website Asynchronous Typeahead example uses uib-typeahead="address for address in getLocation($viewValue)". My guess is that your error message is saying that it's expecting for, not as in the directive. I don't understand this so I could be wrong! :-)

Angular directive default to checked

I have a directive (element) that I created for a "switch", that uses input[checkbox].
As long as the initial state of the checkbox is to be NOT checked, everything works fine. But if I want to set the initial state to checked (based on my controller value), then it's always the opposite. Why is the value not checked when the controller variable says it should be?
Html
<a-switch toggle-state="vm.doSomething"></a-switch>
Directive Html
<div class="switch-container">
<input type="checkbox" id="switch" />
<label for="switch" class="pm-switch"></label>
</div>
Javascript controller
vm.doSomething = {
state: true
};
Directive
angular.module('material')
.directive('aSwitch', [
'$timeout', function($timeout) {
return {
templateUrl: 'elements/material/switch/switch.html',
transclude: false,
restrict: 'E',
scope: {
toggleState: '=',
},
link: function (scope, element) {
element.on('click touchstart', function (event) {
if (event.srcElement && event.srcElement.id && event.srcElement.id === "switch") {
event.stopPropagation();
$timeout(function() {
scope.toggleState.state = !scope.toggleState.state;
});
}
});
}
};
}
]);
I realize that in order to set the checked state of a generic
<input type="checkbox" />
I just need to add the attribute "checked", like
<input type="checkbox" checked />
but how do I do that if it's inside my directive's html?
As you are using isolated scope which has scope: { toggleState: '='} in it, you should have directly bind that toggleState.state to the input box inside template, so that the link function code will get remove as toggleState has been two way binded with your controller scope variable using toggle-state attribute
Directive.html
<div class="switch-container">
<input type="checkbox" id="switch" ng-model="toggleState.state"/>
<label for="switch" class="pm-switch"></label>
</div>

Invalidate select when the first option is selected

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.

How to validate forms step by step with bootstrap and angular form?

I find this example step-by-step form with bootstrap and angularjs
How can I validate the email before jump to step 2 ??
or block the step jump until the fields are full??
function RegisterCtrl($scope, $location) {
$scope.steps = [
'Step 1: Team Info',
'Step 2: Campaign Info',
'Step 3: Campaign Media'
];
....some code
First, define your model in your controller:
function RegisterCtrl($scope, $location) {
$scope.step1 = {
name: '',
email: '',
password: '',
passwordc: ''
};
//...
Bind it to your form fields:
<input type="text" id="inputEmail" ng-model="step1.email" placeholder="Email">
Next, do your validation inside gotoStep():
$scope.goToStep = function(index) {
if (!$scope.step1.email.match(/[a-z0-9\-_]+#[a-z0-9\-_]+\.[a-z0-9\-_]{2,}/)) {
return window.alert('Please specify a valid email');
}
//...
Obviously alert is not great so use jQuery to focus() and add the Bootstrap classes (control-group warning) to highlight the field.
http://jsfiddle.net/xayzP/
You should use directives to test your form field vadility, e.g:
app.directive('email', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
if (viewValue && viewValue.match(/[a-z0-9\-_]+#[a-z0-9\-_]+\.[a-z0-9\-_]{2,}/)) {
// it is valid
ctrl.$setValidity('email', true);
return viewValue;
} else {
// it is invalid, return undefined (no model update)
ctrl.$setValidity('email', false);
return undefined;
}
});
}
};
});
In your html, you need to add the directive to your input field. You can show error messages if a field is not valid using the myForm.email.$error object:
<input type="text" name="email" id="inputEmail" placeholder="Email" ng-model="email" email required>
<span ng-show="myForm.email.$error.email" class="help-inline">Email invalid</span>
<span ng-show="myForm.email.$error.required" class="help-inline">Email required</span>
You can disable the next link until the form is valid by using myForm.$invalid on ng-class:
<li ng-class="{disabled: myForm.$invalid}" >
<a ng-model="next" ng-click="incrementStep(myForm)">Next Step →</a>
</li>
See example.

Categories