compile function in directive not evaluating correctly - javascript

I have a directive called validate that transcludes a form and automatically validates the form based on the built in angular input validation directives. Part of this directive's job is to loop through the child inputs on the form and add appropriate tooltips for data validation. This takes place in the compile portion of the directive. The problem is that the data bindings I set in the compile function don't evaluate in html. For example
app.directive('validate', ["$timeout", "$compile", "gsap", function ($timeout, $compile, gsap) {
return {
scope: {
name: "#"
},
restrict: 'E',
replace: true,
controller: function ($scope) {
$scope.validate = {};
},
template: '<form name="{{name}}" ng-transclude></form>',
transclude: true,
compile: function compile(element, attr) {
//wrap this in a timeout function and wait for children to be available
//Have also tried this in the postLink function to the same result
$timeout(function () {
var selective = element.find('.validate');
if (selective.length > 0) {
$.each(selective, function (k, v) {
v.attr({
"tooltip": '{{validate.' + $(v).attr("name") + '}}',
"tooltip-trigger": '{{{true: "invalid", false: "valid"}[{{name}}.' + $(v).attr("name") + '.$invalid]}}'
});
});
} else {
$.each(element.find('input'), function (k, v) {
$(v).attr({
"tooltip": '{{validate.' + $(v).attr("name") + '}}',
"tooltip-trigger": '{{{true: "invalid", false: "valid"}[{{name}}.' + $(v).attr("name") + '.$invalid]}}'
});
});
}
});
return {
post: function postLink(scope, elem, attr, controller) {
//...a whole bunch of validation code, all works fine...
//should compile with attributes and resolved databindings
$compile(scope, elem, attr, controller);
}
};
}
};
}]);
This evaluates to the following in my DOM
<input ng-model="username" type="email" placeholder="Username" name="username" ng-required="true" ng-minlength="2" class="ng-pristine ng-invalid ng-invalid-required ng-valid-email ng-valid-minlength" required="required" tooltip="{{validate.username}}" tooltip-trigger="{{{true: "invalid", false: "valid"}[{{name}}.username.$invalid]}}">
As you can see, the attributes are set, but the data bindings are not evaluating as i would expect them to

Fixed it. For anyone curious, the compile function syntax is $compile(elem)(scope) I forgot the scope to compile against.

Related

Reuse directive multiplie times with dynamic attributes in another directive's template

What i want to do is to be able to use a directive with different attributes in the same ng-app. The main goal is to run different code when the directive's input (ng-model) changes.
This is what i have now:
app.directive('customInput',
function ($compile) {
var customInputDefinitionObject = {
restrict: 'E',
replace: true,
scope: {
ident: '#'
},
template: '<input type="text" >',
controller: 'customInputController',
compile: function (tElement, tAttrs) {
$('input').removeAttr('ident')
.attr('ng-model', tAttrs.ident)
.attr('ng-change', tAttrs.ident + 'Change()');
var elemLinkFn = $compile(tElement);
return function (scope, element) {
elemLinkFn(scope, function (clone) {
element.replaceWith(clone);
})
}
}
}
return customInputDefinitionObject;
});
It works well in html e.g.:
<custom-input ident="var1"></custom-input>
<custom-input ident="var2"></custom-input>
i'm going to get to input with different ng-model and ng-change function, the controller uses dynamic names to get the $scope variables( $scope.var1Change).
The problem start when i want to use this directive inside another template.
app.directive('customInputGroup', function ($compile) {
var customInputGroupDefinitonObject = {
restrict: 'E',
replace: true,
scope: {
rident: '#',
},
template:''+
'<div>'+
'<custom-input id="first"></custom-input>'+
'<custom-input id="second"></custom-input>'+
'</div>',
controller: 'customInputGroupController',
compile: function (elem, attrs) {
$('#first', elem).removeAttr('id').attr('ident', attrs.rident + 'Start');
$('#second', elem).removeAttr('id').attr('ident', attrs.rident + 'End');
var rangeLinkFn = $compile(elem);
return function (scope, element) {
rangeLinkFn(scope, function (clone) {
element.replaceWith(clone);
})
}
}
}
return customInputGroupDefinitonObject;
});
In this case if i'm going to use it inside the HTML e.g.:
<custom-input-group rident='sg'></custom-input-group>
what i get rendered:
<div>
<input ng-model="sgEnd" ng-change="sgEndChange()">
<input ng-model="sgEnd" ng-change="sgEndChange()">
<input ng-model="sgEnd" ng-change="sgEndChange()">
</div>
For the 3rd rendered input the ng-change does not working.
If set terminal:ture in the inputGroup directive i get only to "input" rendered but both of them has the same ng-model and ng-change.
So how can i make it to render something like this:
<div>
<input ng-model="sgStart" ng-change="sgStartChange()">
<input ng-model="sgEnd" ng-change="sgEndChange()">
</div>
And if u know how would u be so nice to let me know only the "how" but the "why" aswell.
Thank you in advance.

Angular ng-change not working on directive's template

I'm trying to make a hinting ajax search box with an Angular directive. I'm still begining to match data and here is what I have:
function hintSearch () {
return {
restrict: 'E',
replace: true,
template: '<div class="search_hint"><label><input type="search" ng-change="query()"></label><ul class="results"><li class="hint" ng-repeat="hint in hints | limitTo: 8" ng-bind="hint" ng-click="hint_selected(hint)"></li></ul></div>',
scope: {},
link: function(scope, element, attrs){
scope.hints = ["client1", "client2"];
scope.hint_selected = function(){
console.log("hint selected");
}
scope.query = function(){
console.log("query php");
scope.hints = ["client1", "client2", "client3"];
}
}
}
}
The problem is that the ng-change gives me an error. With ng-click or ng-keypress it works perfectly so it makes no sense! Any ideas?
This is the error it throws:
angular.js:13550 Error: [$compile:ctreq] http://errors.angularjs.org/1.5.5/$compile/ctreq?p0=ngModel&p1=ngChange
at Error (native)
at https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:6:412
at gb (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:71:251)
at n (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:66:67)
at g (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:58:305)
at g (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:58:322)
at n (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:65:473)
at g (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:58:305)
at n (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:65:473)
at g (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js:58:305)
From error page:
This error occurs when HTML compiler tries to process a directive that
specifies the require option in a directive definition, but the
required directive controller is not present on the current DOM
element (or its ancestor element, if ^ was specified).
This is the source code of ng-change.
var ngChangeDirective = valueFn({
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
ctrl.$viewChangeListeners.push(function() {
scope.$eval(attr.ngChange);
});
}
});
ng-model is required for ng-change, there is no ng-model in your input.
<input type="search" ng-change="query()">
Add ng-model to your input, hope that will solve your problem.
<input type="search" ng-model='myModel' ng-change="query()">

Encrypt ngModel value in AngularJS

I want to encrypt (using any algorithm) value of a ngModel. Only the $modelValue should be encrypted and view value should be plain text.
To do so, I came up with a small custom directive:-
angular.module('utilityModule').directive('encrypt', function() {
var aesUtil = new AesUtil(128, 10);
return {
restrict: 'A',
require: 'ngModel',
replace: false,
compile: function(tElem, tAttrs) {
var modelName = tAttrs['ngModel'];
var pattern = tAttrs['ngPattern']; // to check if there is ngPattern directive used.
return {
pre: function(scope, element, attrs, fn) {
// to avoid encrypting on every key press.
fn.$options = {
updateOn: 'blur'
};
fn.$parsers.push(function(value) {
//encrypt
console.log('parser invoked');
return value ? aesUtil.encrypt(modelName, modelName, modelName, value) : value;
});
fn.$formatters.push(function(value) {
//decrypt
console.log('formatter invoked');
return value ? aesUtil.decrypt(modelName, modelName, modelName, value) : value;
});
fn.$validators.pattern = function(){
// trying to overrule ngPattern directive. DOESN'T HELP!!
return true;
};
// Just for playing around
fn.$validators.amyValid = function(modelValue, viewValue) {
console.log('Custom validator invoked. modelValue=' + modelValue + ' and viewValue=' + viewValue);
return true;
};
},
post: function(scope, element, attrs, fn) {}
};
}
};
});
The directive works except when we have ngPattern used alongwith the ngModel directive. For example:-
<div class="table-responsive" ng-form="testForm">
<input name="test" type="text" ng-model="test" encrypt ng-pattern="/^[0-9]+$/"/>
<br>
{{test}}
</div>
My expectations:-
ngPattern directive should validate using the $viewValue instead of $modelValue.
How can I override the 'patternDirective' directive present in core angular.js?
Or any other suggestions...
UPDATE 1
Just realized that not just ngPattern, all other validations (maxLength, minLength, max, min) should be applied on view value only
UPDATE 2
My debugger shows that the value passed to patternDirective validator is the encrypted one. Please see the attached screenshot.
UPDATE 3
Upgrading to angularjs 1.4.5 fixed the problem. I believe that 1.3.x has validation on model value and not view value.
Upgrading to angularjs 1.4.5 fixed the problem. I believe that 1.3.x has validation on model value and not view value.

angular unable to $watch ng-model in custom directive

I have custom directives tagPickerTag, validationMessageTag and check-valid-article-meta-tags. Usage:
<div class="my-form-group">
<lable for="create_tags">Tags</lable>
<tag-picker-tag ng-model="entityInfo.meta.tags" editable="true" name="tags" check-valid-article-meta-tags></tag-picker-tag>
<validation-message-tag ctrl="form.tags"></validation-message-tag>
</div>
This is how I define these 3 directives
tagPickerTag:
<div class="tag-picker-tag">
tags
<ui-select ng-model="$parent.ngModel" ng-disabled="! editable" multiple tagging tagging-tokens="SPACE|," tagging-label="(custom 'new' label)" title="Select tags" sortable="true" theme="bootstrap" >
<ui-select-match placeholder="Enter Tags...">{{$item}}</ui-select-match>
<ui-select-choices repeat="tag in suggestedTags | filter:$select.search">
{{tag}}
</ui-select-choices>
</ui-select>
<p>Selected: {{ngModel}}</p>
</div>
'use strict'
var helper = require('../../helper.js')
var app = angular.module('custom_directive')
app.directive('tagPickerTag', [function() {
return {
restrict: 'E',
scope: {
editable: '='
},
templateUrl: '/public/common/directive/tag_picker_tag.html',
require: 'ngModel',
link:
function(scope, element, attrs, ngModelCtrl) {
},
controller:
['$scope',
function($scope) {
//todo: get popular tags from server
$scope.suggestedTags = ['superbowl', '2016election']
}]}}])
checkValidArticleMetaTags:
app.directive('checkValidArticleMetaTags', helper.simpleValidationDirective('article', 'meta', 'tags'))
exports.simpleValidationDirective = function(module, nestedInParent, field) {
return function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$validators.checkValid = function(modelValue, viewValue) {
var validationFunction = exports.validation[module]
if (nestedInParent)
validationFunction = validationFunction[nestedInParent]
validationFunction = validationFunction[field]
var message = validationFunction(modelValue)
ctrl.data = exports.dataAppendedWithMessage({}, 'error', message)
return ! message
}
}}}}
In case you are curious what validationFunction in above code is (it should be irrelevant, since the validation directive correctly get the validation error message):
....
,meta: {
tags: passIfListFulfill('tags', 10, 5, 10, false)
}
var passIfListFulfill = function(fieldName, amount, min, max, required) {
return function(input) {
if (!input || input === [])
return messageForNoInput(fieldName, required)
for (var i = 0; i < input.length; i++) {
var token = input[i]
if (token.length < min)
return token + ' is shorter than min: ' + min
else if (token.length > max)
return token + ' is longer than max ' + max
}
return messageForNoMoreThan(fieldName, input, amount)
}
}
ValidationMessageTag:
app.directive('validationMessageTag', [function() {
return {
restrict: 'E',
scope: {
ctrl: '=ngModel'
},
templateUrl: '/public/common/directive/validation_message_tag.html',
controller:
['$scope',
function($scope) {
$scope.$watch('ctrl.data', function(newValue, oldValue) {
$scope.success = newValue ? newValue.success : []
$scope.info = newValue ? newValue.info : []
$scope.warning = newValue ? newValue.warning : []
$scope.error = newValue ? newValue.error : []
}, true)
}]}}])
<div class="validation-message-tag" ng-show="ctrl.$touched && ctrl.data">
<p ng-repeat="message in success track by $index" class="validation-success">{{message}}</p>
<p ng-repeat="message in info track by $index" class="validation-info">{{message}}</p>
<p ng-repeat="message in warning track by $index" class="validation-warning">{{message}}</p>
<p ng-repeat="message in error track by $index" class="validation-error">{{message}}</p>
</div>
When I enter tags ['a'], in my validation directive, I am able to return false and assign a string "a" is too short to ctrl (which means my validation directive is correct).
but this message does not get passed into my validation_message_tag to display. i.e. the $watch callback is not invoked.
validtion_message_tag works fine for and tags, so i think the problem maybe the implementation of my tagPickerTag custom directive.
the $watch callback is not invoked.
So I couldn't get a simple scenario of $watch to work.
My thought was that ng-model="" is two way bound, you pick it up in the directive as scope: { "ngModel" : "=" } which is a two way binding, so when the value is changed you should see it reflected. So you wouldn't need a $watch. But I tried both however, neither worked.
So I used events instead.
plnkr
$scope.$broadcast(EventNames.statusChange, vm.success)
scope.$on(EventNames.statusChange, function (e, val) { scope.show = val });
A side note, to prevent 'Magic Strings' I made a constant out of the event names. This should help eliminate developer spelling mistakes
Done it with a factory as well. With a factory you don't rely on the $scope so doing controllerAs stays less verbose in the controller dependency list. Secondly, if it ain't working, you know its because the factory hasn't been used, or a callback hasn't been registered. Rather then messing around with the complex thing that is angular eventing system.
A side note on the Event Aggregator pattern (angular $broadcast and $on), in my opinion this creates code that is far too loosely coupled leading to a lot of speghetti code. Another bad point about it to consider is that event enforces these rules:
Everyone can listen to the event
No one has too listen to the event
By creating a service you can enforce developers to listen to things they're broadcasting. In the registercallbacks function you can throw errors if no one has registered a listener. Also, there is now a dependency on StatusService meaning we are a bit tighter coupling the components. IMHO the sweet spot in code coupling.
Setting and listening:
StatusService.setState(vm.success);
StatusService.registerCallbacks(function (val) { scope.show2 = val });
Implementations of the factory functions:
function setState(value) {
for (var i = 0; i < cbs.length; i++) {
cbs[i](value);
}
}
function registerCallbacks(cb) {
cbs.push(cb);
}
Essentially, they are the same thing, however using a factory in my opinion is safer, and you can potentially abstract some logic out into the factory. Rather than do it in the callback functions.
It turns out that I have a cookie flag isTesting set up and forgot to turn it off. When it's testing, I simply return true for all validators.

Angularjs - ngModel.$setViewValue is not a function

Here is my plunker and the code I can't get to work starts on line 32
http://plnkr.co/edit/pmCjQL39BWWowIAgj9hP?p=preview
I am trying to apply an equivalent to markdown filter onto a directive... I created the filter and tested with manually applying the filter and it works that way,, but I should only use the filter conditionally when the type of content on directive is set to markdown.
I am trying to accomplish this by updating ng-model >>> ngModel.$setViewValue(html) but I am getting an error
ngModel.$setViewValue is not a function.. which makes me thing that the controller is not recognized although it is required by the directive.
Here is a working controller:
var app = angular.module('testOne', ["ngResource", "ngSanitize"]);
app.controller('testOneCtrl', function ($scope) {
$scope.product = {
id:12,
name:'Cotton T-Shirt, 2000',
description:'### markdown\n - list item 1\n - list item 2',
price:29.99
};
});
app.directive("myText", function ($parse) {
return {
restrict: "E",
require: "?ngModel",
scope:{
css: "#class", type: "#type"
},
controller: function ($scope, $element, $attrs) {},
templateUrl: "template.html",
compile: function(elm, attrs, ngModel){
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope,elm,attrs){
scope.$parent.$watch(expFn, function(val){
scope.exp = { val: val };
if ( attrs.type == 'markdown'){
var converter = new Showdown.converter();
var html = converter.makeHtml(val);
//scope.exp.val = html;
ngModel.$setViewValue(html);
ngModel.$render();
}
})
scope.$watch('exp.val', function(val){
expFn.assign(scope.$parent, val)
})
}
}
}
})
This is a filter for markdown which works when applied.. (I would consider using the filter if I could figure out the way to conditionally apply it to existing directive but I'd rather do it with ng-model)
/*
app.filter('markdown', function ($sce) {
var converter = new Showdown.converter();
return function (value) {
var html = converter.makeHtml(value || '');
return $sce.trustAsHtml(html);
};
});
*/
Here is the directive template
<div ng-class="{{css}}"
ng-click="view = !view"
ng-bind-html="exp.val">
</div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
This is the directive in use:
<mb-text ng-cloak
type="markdown"
content-type="product"
value="description"
class="test-one-text-2">
</mb-text>
Why ngModel is empty?
When using require on a directive the controller is passed as the 4th argument to the linking function. In you code you try to reference it as an argument of the compile function. The controller is only instantiated before the linking phase so it could never be passed into the compile function anyway.
The bigger issue is that require can only get a controller of the same element ({ require: 'ngModel' }), or parent elements ({ require: '^ngmodel' } ). But you need to reference a controller from a child element (within the template).
How to get ngModel?
Do not use require at all as you cannot get child element's controller with it.
From angular.element docs:
jQuery/jqLite Extras
controller(name) - retrieves the controller of the current element or its parent. By default retrieves controller associated with the ngController directive. If name is provided as camelCase directive name, then the controller for this directive will be retrieved (e.g. 'ngModel').
Inside the linking function you can get the hold of the controller like so:
var ngModel = elm.find('textarea').controller('ngModel');
I fixed your directive:
here is a plunker: http://plnkr.co/edit/xFpK7yIYZtdgGNU5K2UR?p=preview
template:
<div ng-class="{{css}}" ng-bind-html="exp.preview"> </div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
Directive:
app.directive("myText", function($parse) {
return {
restrict: "E",
templateUrl: "template.html",
scope: {
css: "#class",
type: "#type"
},
compile: function(elm, attrs) {
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope, elm, attrs) {
scope.exp = {
val: '',
preview: null
};
if (attrs.type == 'markdown') {
var converter = new Showdown.converter();
var updatePreview = function(val) {
scope.exp.preview = converter.makeHtml(val);
return val;
};
var ngModel = elm.find('textarea').controller('ngModel');
ngModel.$formatters.push(updatePreview);
ngModel.$parsers.push(updatePreview);
}
scope.$parent.$watch(expFn, function(val) {
scope.exp.val = val;
});
scope.$watch('exp.val', function(val) {
expFn.assign(scope.$parent, val);
});
};
}
};
});

Categories

Resources