angular unable to $watch ng-model in custom directive - javascript

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.

Related

How to add dynamic directive names from JSON with the $interpolate function

I'm trying to dynamically add directive names to my directive from a json object. Angular however is only interpolating the directive name which is pulled from a JSON tree once, Angular is then not recognizing and compiling the dynamic children directives once the name is interpolated.
I have tried adding the interpolate service to my DDO so that I can manually interpolate the JSON values, and then have Angular compile.
I however get undefined for $interpolate(tAttrs.$attr.layout) I'm passing the json object to my isolated scope as layout, when I try to access the attr layout I get undefined. My question is how can I access layout object values in the pre link or before compile so that I can interpolate the values and inject them in.
Or do I need to have angular recompile as described here: How do I pass multiple attributes into an Angular.js attribute directive?
Any help would be great.
{
"containers": [
{
"fluid": true,
"rows": [
{
"columns": [
{
"class": "col-md-12",
"directive": "blog"
}
]
},
{
"columns": [
{
"class": "col-md-6 col-md-offset-3 col-xs-10 col-xs-offset-1",
"directive": "tire-finder"
}
]
}
]
}
]
}
...
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<{{column.directive}}></{{column.directive}}>
</div>
</div>
</div>
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate) {
var controller = function(brainService, $scope, $state) {
$scope.directive = {};
var currentState = $state.current.name;
brainService.getDirectiveScope('wpMain', {}).then(function(response) {
$scope.layout = response.states[currentState];
});
};
var compile = function(tElement, tAttrs, transclude) {
var directiveNames = $interpolate(tAttrs.$attr.layout);
}
return {
restrict: 'E',
// replace: true,
scope: {
layout: '=',
},
controller: controller,
templateUrl: 'directive/wpMain/wpMain.html',
compile: compile
};
};
If you're only dealing with a couple options for what a column might be, I would suggest going with #georgeawg's answer.
However, if you expect that number to grow, what you might opt for instead is something along the following lines:
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<column-directive type="column.directive"></column-directive>
</div>
</div>
and then in your JS...
yourApp.directive('columnDirective', columnDirectiveFactory);
columnDirectiveFactory.$inject = ['$compile'];
function columnDirectiveFactory ($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: function (scope, elem, attrs) {
var newContents = $compile('<' + scope.type + '></' + scope.type + '>')(scope);
elem.contents(newContents);
}
};
}
To the best of my knowledge, Angular doesn't have any built-in facility to choose directives in a truly dynamic fashion. The solution above allows you to pass information about which directive you want into a generic columnDirective, whose link function then goes about the business of constructing the correct element, compiling it against the current scope, and inserting into the DOM.
There was an issue with the promise in my original posted code which was preventing me from recompiling the template with the correct directive names. The issue was that I was trying to access the JSON object in the preLink function, but the promise hadn't been resolved yet. This meant that my scope property didn't yet have data.
To fix this I added my service promise to the directive scope $scope.layoutPromise = brainService.getDirectiveScope('wpMain', {}); to which I then called and resolved in my link function. I managed to have Angular compile all of my directive names from the JSON object, but I had to do it in a very hackish way. I will be taking your recommendations #cmw in order to make my code simpler and more 'Angulary'
This is currently my working code:
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate, $compile) {
var controller = function(brainService, $scope, $state) {
$scope.currentState = $state.current.name;
$scope.layoutPromise = brainService.getDirectiveScope('wpMain', {});
};
var link = function(scope, element, attributes) {
scope.layoutPromise.then(function sucess(response) {
var template = [];
angular.forEach(response.states[scope.currentState].containers, function(container, containerKey) {
template.push('<div class="container' + (container.fluid?'-fluid':'') + '">');
//loop rows
angular.forEach(container.rows, function(row, rowkey) {
template.push('<div class="row">');
angular.forEach(row.columns, function(column, columnKey) {
template.push('<div class="' + column.class + '">');
template.push('<' + column.directive +'></' + column.directive + '>')
template.push('</div>');
});
template.push('</div>');
});
template.push('</div>');
});
template = template.join('');
element.append($compile(template)(scope));
})
};
return {
scope: true,
controller: controller,
link: link
};
};

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.

compile function in directive not evaluating correctly

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.

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);
});
};
}
};
});

Directive Isolate Scope 1.2.2

I'm working with Angular version 1.2.2 for the first time and trying to make a simple directive that uses isolate scope with '=' binding to pass in an object. I've done this a few times before so I'm wondering if maybe there was a change in 1.2.2 that changed this?
Here is my directive:
.directive('vendorSelector', function (VendorFactory) {
return {
restrict: 'E',
replace: true,
scope: { vendorId: '=' },
template: '<select ng-model="vendorId" ng-options="id for id in vendorIds">' +
'<option value="">-- choose vendor --</option>' +
'</select>',
link: function (scope, element, attrs) {
VendorFactory.getVendorIds().then(function(result) {
scope.vendorIds = result;
});
}
}
})
My HTML template using the directive is as follows:
<div class="padding">
<vendor-selector vendorId="someValue"></vendor-selector>
{{ someValue }}
</div>
And the backing controller:
.controller('AddProductController', function($scope, ProductFactory, AlertFactory) {
$scope.vendorId = 0;
$scope.someValue = undefined;
})
I've tried using both $scope.someValue and $scope.vendorId as the supplied object in the html template. In both cases the error I'm getting back is Expression 'undefined' used with directive 'vendorSelector' is non-assignable!. Am I missing something obvious that is preventing these values from being 2-way bound in the isolate scope?
In your html:
<vendor-selector vendorId="someValue"></vendor-selector>
Change vendorId="someValue"
to vendor-id="someValue"
HTML attributes are case insensitive so to avoid confusion Angular converts all camel cased variables (vendorId) to snake case attributes (vendor-id).
So someValue wasn't bound to vendorId. Resulting in vendorId being undefined in the template. And thus your error.

Categories

Resources