ng-change and ng-focus not working in custom input directive - angularjs

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/.

Related

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 nested directive $pristine and $dirty settings

I'm having issues with a directive I am writing.
Within the directive's template there is also another element directive.
Essentially the outer directive is a decorator for the inner, adding more functionality..
The issue that I am having is that the $pristine and $dirty values are not being set as I would have expected.
I have amended the fiddle below to demonstrate a similar scenario..
(Code follows:)
HTML
<body ng-app="demo" ng-controller="DemoController">
<h3>rn-stepper demo (3/5)</h3>
Model value : {{ rating }}<br>
<hr>
<div ng-model="rating" rn-stepper></div>
</body>
JS
angular.module('demo', [])
.controller('DemoController', function($scope) {
$scope.rating = 42;
})
.directive('test', function() {
return {
restrict: 'E',
scope: {
ngModel: '=ngModel'
},
template: '<input type="text" ng-model="ngModel"></input>'
};
})
.directive('rnStepper', function() {
return {
restrict: 'AE',
scope: {
value: '=ngModel'
},
template: '<button ng-click="decrement()">-</button>' +
'<div>{{ value }}</div>' +
'<button ng-click="increment()">+</button>' +
'<test ng-model="value"></test>',
link: function(scope, iElement, iAttrs) {
scope.increment = function() {
scope.value++;
}
scope.decrement = function() {
scope.value--;
}
}
};
});
http://jsfiddle.net/qqqspj7o/
The model is shared as expected and when I change the value in either the text input or using the slider, the binding works - however if I update the value in the text input, only the text input is marked as ng-dirty - the element directive itself remains as ng-pristine as does the outer div.
I don't understand why this is and the values are not propagated to the element? Is that expected behaviour - if so, how do I propagate the ng-dirty etc values to the element directive and the outer div..
Note: I can only use Angular v 1.2.x as the code needs to be compatible with IE8.
Thanks in advance..
Generally in directives you should avoid =value binding, and work directly with ngModelController.
This topic is a bit complicated for discussion here, but there are many great tutorias on the web I point you to this one:
using ngModelController it explains basics of working with ngModel and also tells bit about decorators.
When you work directly with ngModel you can set validity and state (dirty/touched/pristine) directly in your code, you can also set model value via $setViewValue().

Input directive with ng-message validation

I would like to know if something like this is possible in angular
<form name='form'>
<input-directive-with-ngmessages1 ... inputname='field1' form='form' />
<input-directive-with-ngmessages2 ... inputname='field2' form='form' />
</form>
I know that validation system in angular is working based on form name, but I would like to make components from inputs, move ng-messages inside custom input directives, passing just form name to it, with possiblity to get overall form validation status,
I was looking about dynamic forms/validation on stackoverflow, but I didn't find something like above example, thx in advance
Definitely doable. My suggestion: Require an ngModel to make a custom control (as described here) use transclusion for the messages. Sample code:
app.directive('inputDirectiveWithNgmessages', function() {
return {
restrict: 'E',
template:
'<input type="text" ng-model="ctrl.model" name="ctrl.inputname" />' +
'<div ng-messages="ctrl.$error" role="alert" ng-transclude>' +
// element content, i.e. the messages will be transcluded here
'</div>',
transclude: true,
scope: {},
require: ['ngModel', 'inputDirectiveWithNgmessages'],
link: function(scope, elem, attrs, ctrls) {
var ngModel = ctrls[0];
var inputDirectiveWithNgmessages = ctrls[1];
inputDirectiveWithNgmessages.inputname = attrs.inputname;
inputDirectiveWithNgmessages.setModel(ngModel);
},
controllerAs: 'ctrl',
controller: function($scope) {
var self = this;
this.model = null;
this.setModel = function(ngModel) {
this.$error = ngModel.$error;
ngModel.$render = function() {
self.model = ngModel.$viewValue;
};
$scope.$watch('ctrl.model', function(newval) {
ngModel.$setViewValue(newval);
});
};
}
};
});
Usage is quite simple - place the messages inside the element:
<input-directive-with-ngmessages ng-model="model1" inputname="field1" ng-required="true">
<div ng-message="required">Required field</div>
</input-directive-with-ngmessages>
No need to specify the form name.
And a fiddle: http://jsfiddle.net/12sf82p3/
THE CATCH: Standard validators we have been comfortably using with inputs, e.g. ng-pattern, do NOT work out of the box in this (except for the ng-required). You see ng-pattern is not a directive; it is handled as an attribute by Angular's standard input directive. The workaround is to implement the validators you want as directives and place them on the <input-directive-with-ngmessages>, e.g.:
<input-directive-with-ngmessages ng-model="..." inputname="..." my-pattern="[A-Z][0-9]">
The my-pattern directive will use the standard $validators pipeline of ngModel to implement regular expression validation.
A benefit of this is that the template of the directive can be tweaked to suit any needs. E.g. it could create form element markup for Twitter's Bootstrap.
Finally you may want to take a look at egkyron for an alternative to form validation, i.e. model based validation.

How to add arbitrary attributes to an angular directive for data validation

I am attempting to create an angular directive that will be a custom tag for input fields in our application. Essentially what it will do is create the label, input field and the various bootstrap classes so there is a consistent look to them.
Along with that I would like it if I could add the various data validators that are appropriate for the particular input (such as required and custom validators) as attributes of the custom tag and then have those added to the input field and thus perform validation on that.
I have figured out a way that appears to put the attributes on the input field and the custom validator is getting called and properly evaluating the data, but the form never seems to think that the data is invalid. I think I am having a scope problem where the input being invalid is being set on the directive's scope rather than the parent scope but I'm not 100% sure about that and even if it is the problem I don't know how to fix it.
Here's a sample of what I'd like one of the tags to look like
<textinput ng-model="TestValue" name="TestValue" text="Label Text" config="GetConfigurationForm()" ngx-ip-address required></textinput>
which I want to generate something like
<div class="row">
<div class="form-group" ng-class="{ 'has-error': IsInvalid() }">
<label for="{{name}}" class="control-label">{{text}}</label>
<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}" ngx-ip-address required>
</div>
</div>
Note that the ngx-ip-address and required have been moved to the input field attributes.
My controller looks like the following (sorry it's so long)
var app = angular.module('test', []);
app.directive('ngxIpAddress', function()
{
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attributes, ngModel)
{
ngModel.$validators.ngxIpAddress = function(modelValue, viewValue)
{
// Value being blank is OK
if (ngModel.$isEmpty(modelValue))
return true;
// If the string starts with a character then
// this is not valid
if (isNaN(parseInt(viewValue[0])))
return false;
var blocks = viewValue.split(".");
if(blocks.length === 4)
{
return blocks.every(function(block)
{
return parseInt(block, 10) >= 0 && parseInt(block, 10) <= 255;
});
}
return false;
};
}
};
});
app.directive('textinput', function ()
{
return {
restrict: 'E',
scope: {
//# reads the attribute value, = provides two-way binding, & works with functions
ngModel: '=',
name: '#',
text: '#',
config: '&'
},
controller: function($scope) {
$scope.IsInvalid = function()
{
var getConfigurationFunction = $scope.config();
if (!getConfigurationFunction || !getConfigurationFunction[$scope.name])
return false;
return getConfigurationFunction[$scope.name].$invalid;
};
},
link: function(scope, element, attributes) {
var inputElement = element.find("input");
for (var attribute in attributes.$attr)
{
if (attribute !== "ngModel"
&& attribute !== "name"
&& attribute !== "text"
&& attribute !== "config")
{
inputElement.attr(attribute, attributes[attribute]);
}
}
},
template: '<div class="row">' +
'<div class="form-group" ng-class="{ \'has-error\': IsInvalid() }">' +
'<label for="{{name}}" class="control-label">{{text}}</label>' +
'<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}">' +
'</div>' +
'</div>'
};
});
app.controller(
"TestController",
[
"$scope",
function TestController(_scope)
{
_scope.TestValue = "TestTest";
_scope.GetConfigurationForm = function()
{
return _scope.ConfigurationForm;
};
}
]
);
If I put the attributes in the actual template then everything works as expected and the control turns red if the data isn't an ip address. When I add the attributes by moving them that doesn't work.
Here is a plunkr showing what I've got so far: http://plnkr.co/edit/EXkz4jmRif1KY0MdIpiR
Here is a plunkr showing what I'd like the end result to look like where I've added the tags to the template rather than the tag: http://plnkr.co/edit/mUGPcl1EzlHUiMrwshCr
To make this even more fun, in the future I will actually need to pass in a value to the data validation directives from the outside scope as well, but I'd like to get this working first.
Here you may find the correct answer.
The reasons of this issue are:
the attr will convert the attribute from ngxIpAddress to ngxipaddress, namely from the uppercase to lowercase, you can find this issue from this link. To solve it, just pass ngx-ip-address as parameter for function attr.
$compile(inputElement)(scope); need to be added into directive, when one directive is used in another directive. Here is one link.

AngularJS: Can't get access value of isolate scope property in directive

Problem:
In a directive nested within 3 ui-views, I can't access the values of my isolate scope. scope.values returns {} but when I console.log scope I can see all the values on the values property.
In a different app I can make this works and I converted this one to that method as well but it still doesn't work and I'm tracing the routes, ctrl's and I can't find the difference between the two.
Where I'm trying to access it from
Init App > ui-view > ui-view > ui-view > form-elements > form-accordion-on
What I'm working with:
The view
<ul class='form-elements'>
<li
class='row-fluid'
ng-hide='group.hidden'
ng-repeat='group in main.card.groups'
card='main.card'
form-element
values='values'
group='group'>
</li>
</ul>
This directive contains tons of different form types and calls their respective directives.
.directive('formElement', [function () {
return {
scope: {
values: '=',
group: '='
},
link: function(scope, element) {
l(scope.$parent.values);
element.attr('data-type', scope.group.type);
},
restrict: 'AE',
template: "<label ng-hide='group.type == \"section-break\"'>" +
"{{ group.name }}" +
"<strong ng-if='group.required' style='font-size: 20px;' class='text-error'>*</strong> " +
"<i ng-if='group.hidden' class='icon-eye-close'></i>" +
"</label>" +
"<div ng-switch='group.type'>" +
"<div ng-switch-when='accordion-start' form-accordion-on card='card' values='values' group='group'></div>" +
"<div ng-switch-when='accordion-end' form-accordion-off values='values' class='text-center' group='group'><hr class='mbs mtn'></div>" +
"<div ng-switch-when='address' form-address values='values' group='group'>" +
"</div>"
};
}])
This is the directive an example directive.
.directive('formAccordionOn', ['$timeout', function($timeout) {
return {
scope: {
group: '=',
values: '='
},
template: "<div class='btn-group'>" +
"<button type='button' class='btn' ng-class='{ active: values[group.trackers[0].id] == option }' ng-model='values[group.trackers[0].id]' ng-click='values[group.trackers[0].id] = option; toggleInBetweenElements()' ng-repeat='option in group.trackers[0].dropdown track by $index'>{{ option }}</button>" +
"</div>",
link: function(scope, element) {
console.log(scope) // returns the scope with the values property and it's values.
console.log(scope.values); // returns {}
})
// etc code ...
Closely related to but I'm using = on every isolate scope object:
AngularJS: Can't get a value of variable from ctrl scope into directive
Update
Sorry if this is a bit vague I've been at this for hours trying to figure out a better solution. This is just what I have atm.
Update 2
I cannot believe it was that simple.
var init = false;
scope.$watch('values', function(newVal, oldVal) {
if (_.size(newVal) !== _.size(oldVal)) {
// scope.values has the value I sent with it!
init = true;
getInitValues();
}
});
But this feels hacky, is there a more elegant way of handling this?
Update 3
I attach a flag in my ctrl when the values are ready and when that happens bam!
scope.$watch('values', function(newVal) {
if (newVal.init) {
getInitValues();
}
});
The output of console.log() is a live view (that may depend on the browser though).
When you examine the output of console.log(scope); scope has already been updated in the meantime. What you see are the current values. That is when the link function is executed scope.values is indeed an empty object. Which in turn means that values get updated after the execution of link, obviously.
If your actual problem is not accessing values during the execution of link, the you need to provide more details.
Update
According to your comments and edits you seem to need some one time initialization, as soon as the values are there. I suggest the following:
var init = scope.$watch('values', function(newVal, oldVal) {
if (newVal ==== oldVal) { //Use this if values is replaced, otherwise use a comparison of your choice
getInitValues();
init();
}
});
init() removes the watcher.

Resources