Accessing directive values in controller - angularjs

I realize there are similar questions to this already on SO but I can't find the solution to my problem.
I have the following directive which extracts a key and value for the input box that the cursor leaves (blur):
.directive('updateOneField', function() {
return {
restrict: 'A',
scope: [],
link: function(scope, element, attr) {
element.bind('blur', function() {
var key = attr.ngModel.split('.');
key = key[key.length - 1];
// Get the input value
var value = element[0].value;
});
}
};
});
This will potentially be used across multiple controllers so my question is how do I access the key and value values from any controller?

Yes, you can pass a controller scope variable to your directive and use this variable to access the value from the directive.
Example
<input type="text" update-one-field my-scope="myVariable" />
Here, myVariable is your controller's variable.
$scope.myVariable = {};
Now, update your directive like this,
.directive('updateOneField', function() {
return {
restrict: 'A',
scope: {
myScope: '='
},
link: function(scope, element, attr) {
element.bind('blur', function() {
var key = attr.ngModel.split('.');
key = key[key.length - 1];
// Get the input value
var value = element[0].value;
// Assign key and value to controller variable
scope.myScope = {};
scope.myScope.Key = key;
scope.myScope.Value = value;
});
}
};
});
Now, you can access key & value from the controller just like,
// Code inside your controller
$scope.myVariable.Key; // Get key from the directive
$scope.myVariable.Value; // Get value from the directive
Hope this will help. If you have any doubt on this, please feel free to add comment.

You are trying to access the value of ngModel if I understand right, the most "angular-way" of doing this is to require ngModelController in your directive like so;
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
element.bind('blur', function() {
var model = ngModelCtrl.$modelValue;
});
}
};
You can find more information about ngModelController here

you have a typo in the directive declaration :
.directive('updateOneField', function() {
return {
restrict: 'A',
// her -------------------------------> scope: [],
scope : {},
link: function(scope, element, attr) {
element.bind('blur', function() {
var key = attr.ngModel.split('.');
key = key[key.length - 1];
// Get the input value
var value = element[0].value;
});
}
};
});

Related

Validation of complex form element

I have created a complex form element to avoid code duplication.
However I can't make it to behave same as normal input field.
HTML
<input name="first" ng-model="ctrl.first" type="text" required is-number />
<complex-input name="second" ng-model="ctrl.second" required></complex-input>
JS/ng
// main input directive
app.directive("complexInput", function(){
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel'
},
template: "<div><input ng-model='passedModel' is-number type='text' child-element /></div>",
link: function(scope, elem, attr, modelCtrl) {
angular.extend(modelCtrl.$validators, scope.childValidators || {});
}
}
});
// is number validator
app.directive('isNumber', function(){
return {
require: "^?ngModel",
link: function(scope, elem, attr, modelCtrl) {
modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return !isNaN(value);
};
}
}
});
// hacky directive to pass back validators from child field
app.directive('childElement', function(){
return {
require: "^?ngModel",
priority: 10,
link: function(scope, elem, attr, modelCtrl) {
if (!modelCtrl) return;
scope.childValidators = modelCtrl.$validators;
}
}
});
When I run it content of both fields errors is following.
On init:
First: {"required":true}
Second: {"required":true}
If I enter string:
First: {"isNumber":true}
Second: {**"required":true**,"isNumber":true}
If I enter number:
First: {}
Second: {}
I would expect both input and complex-inputto behave same. Problem is obviously that is-number validation on inner input is blocking model on outer complex-input so it's value is not set, unless you enter number.
What am I doing wrong?
Is there a nicer/cleaner way to do this and possibly avoid the ugly childElement directive?
Please find test plnkr here: https://plnkr.co/edit/Flw03Je1O45wpY0wf8om
UPDATE: Complex input is not a simple wrapper for input. In reality in can have multiple inputs that together compile a single value.
You can solve both problems (the childElement and the correct validation) by letting complex-element be only a wrapper around the real input field.
To do this :
The complex-element directive has to use something else than name, for example "input-name"
The input in the complex-element directive template need to use that name
You need to pass from complex-element to the input field whatever else you need (validations, events etc..)
For example, the following is your code modified and works as you expect :
var app = angular.module("myApp", []);
app.controller("MyCtrl", function($scope){
var vm = this;
vm.first = "";
vm.second = "";
});
app.directive("complexInput", function(){
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel',
name: '#inputName'
},
template: "<div><input ng-model='passedModel' name='{{name}}' is-number type='text'/ required></div>",
link: function(scope, elem, attr, modelCtrl) {
angular.extend(modelCtrl.$validators, scope.childValidators || {});
}
}
});
app.directive('isNumber', function(){
return {
require: "^?ngModel",
link: function(scope, elem, attr, modelCtrl) {
modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return !isNaN(value);
};
}
}
});
HTML
<p>COMPLEX:<br/><complex-input required input-name="second" ng-model="ctrl.second"></complex-input></p>
See the plunker here : https://plnkr.co/edit/R8rJr53Cdo2kWAA7zwJA
Solution was to add ng-model-options='{ allowInvalid: true }' on inner input.
This forces inner input to update model even if it's invalid.
However more nicer solution would be to pass entire model from child elements to parent directive and then iterate through their $validators.
app.directive('childElement', function(){
return {
require: "^?ngModel",
priority: 10,
link: function(scope, elem, attr, modelCtrl) {
if (!modelCtrl) return;
scope.childModels.push(modelCtrl);
}
}
});
Complex input here has two inputs that combined give a final value.
First one has to be number but it's not required, while the second one is required.
app.directive("complexInput", function(){
function preLink(scope, elem, attr, modelCtrl) {
// create local scope value
scope.inner1 = "";
scope.inner2 = "";
scope.childModels = [];
}
// do some nice mapping of inner errors
function mapKeys(index, validator) {
return validator + "." + index;
}
function postLink(scope, elem, attr, modelCtrl) {
// copy value on change to passedModel
// could be complex state
scope.$watch('inner1', function(value){
scope.passedModel = scope.inner1 + scope.inner2;
});
scope.$watch('inner2', function(value){
scope.passedModel = scope.inner1 + scope.inner2;
});
scope.childModels.forEach(function(childModel, index){
childModel.$viewChangeListeners.push(function(){
Object.keys(childModel.$validators).forEach(function(validatorKey){
modelCtrl.$setValidity(mapKeys(index, validatorKey), !childModel.$error[validatorKey]);
});
});
});
}
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel'
},
template: "<div><input ng-model='inner1' is-number type='text' child-element /><br/><input ng-model='inner2' required type='text' child-element /></div>",
compile: function(element, attributes){
return { pre: preLink, post: postLink };
}
}
});

Input does not reflect ngModel value

I am trying to do some custom validation based on a json object a user gives me.
However the input field visually does not show the value of the ngModel property. I added a plunkr to illustrate the problem.
'use strict';
angular.module('zendantennesApp')
.directive('validation', function ($compile, $parse) {
return {
scope: {
validation: '#',
ngModel: '#'
},
require: "?ngModel",
restrict: 'A',
compile: function(el, attrs) {
el.removeAttr('validation');
el.attr('ng-blur', 'evaluateExpression()');
el.attr('ng-focus', 'assignOriginalValue()');
var fn = $compile(el);
return function(scope, element, attrs, ngModel){
ngModel.$render = function(){
$(element).val(ngModel.$viewValue);
};
fn(scope);
}
},
controller: function($scope){
$scope.originalValue = $scope.ngModel;
$scope.validationObject = JSON.parse($scope.validation.replace(/'/g, '"'));
$scope.evaluateExpression = function(){
console.log($scope.validationObject);
};
$scope.assignOriginalValue = function(){
$scope.originalValue = $scope.ngModel;
console.log($scope.originalValue);
}
}
}
});
https://plnkr.co/edit/1qYxCiSZWHgVeN9CEpxw?p=preview
validation directive will have isolated scope and hence parent scope value will not be accessible unless you explicitly mention during compile. Replace fn(scope); by fn(scope.$parent);
Updated Plunker

angular: How to change the data type using $formatters and $parsers?

Using ngModel.$formatters and ngModel.$parsers i'm trying to create a directive to change the data-type from model to view and vice versa.
The problem is when i use <input type="number">
In that case toView receives 'undefined' as value, but not string data. ("1234")
If i remove type=number works fine, but the input element is not what i want.
There are any way to get it working?
if not, there are another way to achieve this aim? (the model MUST be string, the input MUST be type="number")
PLUNK
javascript
var app = angular.module('module', []);
app.controller('MainCtrl', function($scope) {
$scope.model = {};
$scope.model.number = "1234";
});
app.directive('numberConverter', function() {
return {
restrict: 'A',
require: 'ngModel',
link: {
pre: function(scope, element, attr, ngModel) {
function toModel(value) {
return "" + value; // convert to string
}
function toView(value) {
return parseInt(value); // convert to number
}
ngModel.$formatters.unshift(toView);
ngModel.$parsers.unshift(toModel);
},
post: function() {}
}
};
});
html
<input type="number" number-converter ng-model="model.number">
Sorry, my original answer wasn't quite right.
Here is an updated directive that will correctly store the value as a string but edit it as a number.
It's important that the priority of the directive be greater than 0 (the default priority) so that it runs after the ngModel directive does. This makes sure that you're adding your formatters and parsers after the defaults (otherwise you're just pushing to an empty list and the default processors will be added after yours anyway).
app.directive('numberConverter', function() {
return {
restrict: 'A',
require: 'ngModel',
priority: 1,
link: function(scope, element, attr, ngModel) {
function toModel(value) {
return "" + value; // convert to string
}
function toView(value) {
return parseInt(value); // convert to number
}
ngModel.$formatters.push(toView);
ngModel.$parsers.push(toModel);
}
};
});
Updated plunkr: http://plnkr.co/edit/l8c1GGBeIX4dawLZJfoC?p=preview

Angular Isolate Scope breaks down?

I have the following markup:
<div class="controller" ng-controller="mainController">
<input type="text" ng-model="value">
<div class="matches"
positions="{{client.positions | filter:value}}"
select="selectPosition(pos)">
<div class="match"
ng-repeat="match in matches"
ng-click="select({pos: match})"
ng-bind="match.name">
Then, inside my matches directive I have
app.directive('matches', function()
{
return {
scope: {
select: '&'
},
link: function(scope, element, attrs)
{
scope.matches = [];
attrs.$observe('positions', function(value)
{
scope.matches = angular.fromJson(value);
scope.$apply();
})
}
}
}
When I do this, I can console log scope.matches, and it does change with the value from my input. However, the last div .match doesn't render anything! If I remove scope: {...} and replace it with scope: true, then it does render the result, but I want to use the & evaluation to execute a function within my main controller.
What do i do?
Use scope.$watch instead, you can watch the attribute select whenever changes are made from that attribute.
app.directive('matches', function()
{
return {
scope: true,
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch(attrs.select, function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
UPDATE: Likewise, if you define select itself as a scope attribute, you must use the = notation(use the & notation only, if you intend to use it as a callback in a template defined in the directive), and use scope.$watch(), not attr.$observe(). Since attr.$observe() is only used for interpolation changes {{}}, while $watch is used for the changes of the scope property itself.
app.directive('matches', function()
{
return {
scope: {select: '='},
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch('select', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
The AngularJS Documentation states:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Not scope properties defined as such in your problem which is defining scope in the directive definition.
If you don't need the isolated scope, you could use $parse instead of the & evaluatation like this:
var selectFn = $parse(attrs.select);
scope.select = function (obj) {
selectFn(scope, obj);
};
Example Plunker: http://plnkr.co/edit/QZy6TQChAw5fEXYtw8wt?p=preview
But if you prefer the isolated scope, you have to transclude children elements and correctly assign the scope of your directive like this:
app.directive('matches', function($parse) {
return {
restrict: 'C',
scope: {
select: '&',
},
transclude: true,
link: function(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(scope, function (clone) {
element.append(clone);
});
scope.matches = [];
attrs.$observe('positions', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
});
Example Plunker: http://plnkr.co/edit/9SPhTG08uUd440nBxGju?p=preview

get initial value of ngModel in directive

I have a directive that requires ngModel. The directive modifies the value stored in ngModel (it implements in-place editing of text). Inside my link function I need to get the value of ngModel before it has been changed.
I tried looking at ngModel.$viewValue, and ngModel.$modelValue. They both eventually get the model's contents, but in the beginning of the directive's life-cycle they get the raw unprocessed angular expression such as {{user.name}}. And I cannot find a way to determine
when the expression has been processed.
Any ideas?
directive('test', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
}
};
})
Use the $parse service:
app.directive('test', function($parse) {
return {
link: function (scope, element, attrs) {
var modelGetter = $parse(attrs.ngModel);
var initialValue = modelGetter(scope);
}
};
});
Or:
app.directive('test', function($parse) {
return {
compile: function compile(tElement, tAttrs) {
var modelGetter = $parse(tAttrs.ngModel);
return function postLink(scope, element) {
var initialValue = modelGetter(scope);
};
}
};
});
Demo: http://plnkr.co/edit/EfXbjBsbJbxmqrm0gSo0?p=preview

Resources